Java基础加强

重点学习内容

  • 面向对象
  • 字符串
  • 接口
  • 容器
  • 异常
  • 泛型
  • 反射
  • 注解
  • I/O

一、面向对象特性

1. 面向对象

​ 面向对象是一种编程思想,是使用类或对象作为组织代码的基本单元,具有继承、封装、多态三大特性。

  • 封装就是控制类的访问权限,只开放出对外的方法,并抽象出接口类,供使用者操作。一方面,让数据更安全,另一方面,也提高了易用性。
  • 继承子类可以使用父类的非私有变量和方法,主要作用就是实现代码复用
  • 多态提高了代码的可拓展性和复用性,是指一个类实例的相同方法在不同情形有不同表现形式。通过同一个上级引用指向不同的子类,使得对同一消息做出不同回应。

2. 重载和重写

​ 重载和重写是面向对象多态性的体现,重载指的是同一个类中的同名方法可以根据不同的参数列表来区分,重写也称覆写,指的是子类可以重写父类的方法。

2.1 重载

​ 在Java中,重载是在编译期确定的,下面的例子可以说明,在编译期,编译器并不知道对象的实际类型,所以在调用方法的时候通过静态类型判断来选择重载的方法。

public void sayHello(Human guy) {
System.out.println("guy say hello");
}

public void sayHello(Man man) {
System.out.println("man say hello");
}

public void sayHello(Woman woman) {
System.out.println("woman say hello");
}

public static void main(String[] args) {
// Man和Woman继承了Human
Human man = new Man();
Human woman = new Woman();
StaticDispatch sd = new StaticDispatch();
sd.sayHello(man); // guy say hello
sd.sayHello(woman);// guy say hello
sd.sayHello((Man)man);// man say hello
sd.sayHello((Woman)woman);// woman say hello
}

2.2 重写

重写是在运行期确定的,通过下面的例子可以看到。

​ 首先根据编译期的重载,两个方法选择了Father.hardChoice(_360()); Father.hardChoice(QQ()); 。在运行时,虚方法表中存放着各个方法的实际入口地址,如果没有重写,子类的方法表中方法的地址就是父类方法表中方法的地址重写之后子类的方法表中方法的地址就会替换为子类重写的方法的地址。所以在运行期,Son的方法表指向的是Son.hardChoice(QQ());

public class Dispatch {

static class QQ{}

static class _360{}

public static class Father{
public void hardChoice(QQ arg) {
System.out.println("father choose qq");
}

public void hardChoice(_360 arg) {
System.out.println("father choose 360");
}
}

public static class Son extends Father{
@Override
public void hardChoice(QQ arg) {
System.out.println("son choose qq");
}

@Override
public void hardChoice(_360 arg) {
System.out.println("son choose 360");
}
}

public static void main(String[] args) {
Father father = new Father();
Father son = new Son();

father.hardChoice(new _360()); // father choose 360
son.hardChoice(new QQ());// son choose qq
}
}

3. public,protect,private修饰符及默认default

​ public可以被任何类调用,protect区别主要是不同包的话,就必须是子类才能访问。(即同包、不同包子类),private呢就只能自己才能访问。

​ default:就默认的访问权限,自己和同包下访问。

4. static,final

​ static用于修饰属性,使的属性属于类,只实例化一次,类及其实例化对象都使用这一个;也可以修饰方法,可以修饰内部类来做静态代码块。

final修饰的类不能继承,修饰的变量值不能改变,final修饰的引用地址不能改,但地址对应的内部内容可以改。

5. 接口和抽象类的区别

  1. 接口的方法都是抽象的,抽象类的方法可以是抽象的,也可以是非抽象的;
  2. 只要不是抽象类abstract,实现接口必须实现所有的类,同时继承抽象类时,必须实现abstract抽象方法。
  3. 接口成员函数都是默认public,抽象类有多种修饰符;
  4. 在Java中类是多实现,单继承的,所以继承抽象类的方法调用比实现类实现接口的方法调用快;

抽象类是用来实现子类的通用特性,有利于代码复用。接口实现不同的模块间的方法名的统一或者涉及多继承的情况都用接口,大部分情况下也都使用的是接口,利于多态的实现。

二、String字符串

下面是我们的案例:进入String,查看源码

/**
* @Description TODO
* @Author rnang0
* @Date 2020/5/26 18:10
* @Version 1.0
**/
public class StringTest {

public static void main(String[] args) {
String str = "abc";
}

}

​ 进入到java.lang.String中

1

1. 存储结构

String实际上是char数组,final声明不可继承。其实这在我们c语言中,是显然的,c语言就没有字符串的概念,全是字符数组,在C++中才被引入String字符串。

private final char value[];

​ 存储结构的变更:jdk8为char[]数组,而9之后就开始使用的是byte[]字节数组,原因如下:

一个char两个字节(16位),7之后String字符串占据了堆空间的主要区域,而字符串中一些拉丁文的编码字符占大多数,它们是只需要一个字节即可存储,这样就会有一半空间浪费,所以采用byte数组来分类存储:中文,UTF-16占2个字节,一些拉丁文的编码字符采用一个字节存储。

2. 不可变性

​ 我们发现,String类本身以及其属性都是final(不可继承)的,这是为什么呢?

//String实际是字符数组value[]
private final char value[];

/** Cache the hash code for the string */
private int hash; // Default to 0

String 申请后不可再变,这样做

  1. 因为可以节约堆空间,多个字符串变量都会指向字符串常量池中同一个字符串;
  2. 在多线程环境下,那么不能变的话就保证了线程安全
  3. 在类加载时描述符都是字符串形式的,不可变就提高了安全性;
  4. 最后就是字符串的不变性让他很适合做hash映射的键,由于本身不变化,所以hashcode也不会变化,那么可以缓存字符串的hashcode提高效率。
String s1 = "abc";
String s2 = "abc";
// 内存中只有一个"abc"对象被创建,同时被s1和s2共享。

面试题:str指向的是堆空间中的String对象,而由于不可变性,在change方法中局部变量str开始是指向堆空间中的String对象,然后又指向字符串常量池中”test ok”,出来后str不变,而数组是可以改变值的。

public class StringTransmit {
String str = new String("good");
char [] ch = {'t','e','s','t'};
public void change(String str,char ch[]){
str = "test ok";
ch[0] = 'b';
}

public static void main(String[] args) {
StringTransmit ex = new StringTransmit();
ex.change(ex.str,ex.ch);
System.out.println(ex.str); //good
System.out.println(ex.ch); //best
}

}

总结:

​ String直接用引号创建时会在字符串常量池(jdk7之前在方法区,后面在堆中)查看是否已经存在,有的话直接返回内存地址,没有的话创建后返回;

​ new String创建时会在堆上创建对象并返回,然后会在常量池中看有没有一样的字符串,有就指向它,没有的话在字符串中创建一个并指向它;

public static void main(String[] args) {
String a = "abc";
String b = "abc";
System.out.println(a == b); //true

String c = new String("abc");
String d = new String("abc");
System.out.println(c == d); //false
}

3. String对象性能优化

3.1 字符串的拼接

​ ‘’abc”+”def”这种在编译期优化,等价于 ‘’abcdef”,所以这样效率最高。而带有变量的str + “abc”这种方式就不一样了,

​ 由于String类的对象内容不可改变,所以每当进行字符串拼接时,总是会在内存中创建一个新的对象。虽然拼接时带有变量的使用 “+” 和StringBuilder拼接,底层都是使用StringBuilder方式,每次拼接,都会构建一个新的String对象,既耗时,又浪费空间,效率都是一样。

3.2 toString方法

StringBuilder的toString方法,约等于new String(“xxx”),区别是字符串常量池中没有。

​ 所以,new StringBuilder("abc").toString().intern();这等价于在字符串常量池中新建了abc,返回其引用。

3.3 StringBuilder概述

​ 查阅java.lang.StringBuilder的API,StringBuilder又称为可变字符序列,它是一个类似于 String 的字符串缓冲区,通过某些方法调用可以改变该序列的长度和内容。

​ 原来StringBuilder是个字符串的缓冲区,即它是一个容器,容器中可以装很多字符串。并且能够对其中的字符串进行各种操作。

它的内部拥有一个数组用来存放字符串内容,进行字符串拼接时,直接在数组中加入新内容。StringBuilder会自动维护数组的扩容。原理如下图所示:(默认16字符空间,超过自动扩充)

3.4 循环拼接

​ 但对于循环拼接,则必须使用StringBuilder的append方法进行拼接。如果不使用append,则会创建n个StrignBuilder对象,则显示是损耗性能的。

String str = "abcdef";
for(int i=0; i<1000; i++) {
str = str + i;
}

// 编译优化
String str = "abcdef";
StringBuilder stringBuilder = new StringBuilder(String.valueOf(str));

for (int i = 0; i < 100; i++) {
str = stringBuilder.append(i).toString();
}

在阿里巴巴Java开发手册1.4节17条明确规定,在循环体内的连接方式要使用StringBuilder的append方法进行拓展。

3.5 intern()方法

​ String类的存储不可变性是为了实现常量池,实现字符串的复用。

​ a指向常量池中的字符串引用,而b创建了两个对象,一个指向的是堆中新建的String对象,另一个是字符串常量池里还有。

String a = "abc"; 
String b = new String("abc");
public native String intern();

​ 我们可以看到intern是个本地方法,返回该字符串的引用。

  1. String a = "abc";
    String b = new String("abc");
    String c = new String("abc").intern();

​ 所以a和c都指向常量池中的同一个字符串(c是去常量池中找abc的引用,无创建,有返回),而b指向堆中的String对象,但是三者的value属性都是指向同一个char数组的

  1. //字符串复用
    String a = "abc";
    String b = new String("abc").intern();
    String c = new String("efg").intern();
  • 如果常量池中有相同的值,就返回常量池中字符串对象的引用。

  • 没有,就把字符串添加到常量池中,返回常量池中该字符串的引用。

    所以a和b都指向常量池中的同一个字符串 a == b,而c指向常量池中的“efg”。

4. String、StringBuffer、StringBuilder

​ 开始讲了StringBuilder,而StringBuffer与其类似,他们都是可变的字符序列,底层依然是char数组/byte数组。

StringBuffer与StringBuilder扩容机制:

默认的容量都是为16,扩容是扩容为原来的2倍 + 2,同时复制,所以频繁的扩容会使效率变低,则初始化时建议指定大小。

异同:

  1. String为不可变的字符序列,1.8及之前底层为char数组,9位byte数组
  2. StringBuffer为可变的字符序列,线程安全的,因为底层上所有的方法相对于StringBuilder都使用synchronized修饰了,所以它滴性能就低,底层依然是char数组/byte数组
  3. StringBuilder为可变的字符序列,不是线程安全的,性能高,底层依然是char数组/byte数组

效率:StringBuilder > StringBuffer > String

三、异常

1. 分类

​ 异常分为Error和Exception

  • Error:JVM无法处理的严重问题,例如资源耗尽。
    • StackOverflowError栈溢出,OOM(OutOfMemoryError)堆溢出
  • Exception:这就是我们常说的异常,偶然因素引起,可以用针对性的代码处理。
    • 数组越界ArrayIndexOutOfBound,空指针异常NullPointException,网络连接中断等

2. 异常Exception分类

​ Exception才是我们常说的异常,因为Error我们无法通过代码处理。

Exception分为编译期异常(javac.exe)、运行时异常(java.exe)

  • 编译时(受检)异常:
    • FileNotFoundException,I/O异常,ClassNotFound等
  • 运行时(不受检)异常:
    • ArrayIndexOutOfBound,NullPointException,ClassCastException(类型转换异常)
    • NumberFormatException(数字转换异常,例如字符串通过Interger的方法想转成数字)
    • ArithmeticException数学异常,例如除以0

3. 异常的处理

3.1 try-catch-finally

​ catch中要明确异常的种类,通过e.printStackTrace()来打印异常,但是它处理的是编译期异常,可能会在运行时发生异常。

3.2 throws

​ throws + 异常类型,在方法上抛出。

​ 与try-catch-finally的区别:try-catch-finally是真滴处理了,throws而是抛给调用者处理。

如何选择?在当前方法中可以使用try-catch-finally,如果方法与其他方法之前存在递进关系(例如web中的controller->service->dao),则可以使用throws抛出去处理。

4. throws与throw的区别

​ throws 是异常处理的一种方式,声明在方法声明外

​ throw 是我们自己手动生成抛出的异常对象,声明在方法体内,一个生成一个处理。

四、泛型

1. 为什么使用泛型

  1. 不指定存储的数据类型,会导致存入其他的数据类型出现安全问题,则泛型进行编译时强类型检查,排查隐患。
  2. 避免了强制类型转换,可能会出现ClassCastExcpetion。
  3. 可以实现通用的算法,处理不同的类型集合,不必纠结于具体的数据类型,易于阅读。

2. 泛型的实现

​ 泛型的实现时采用了类型擦除的机制,任何的具体数据类型都会在编译后被擦除。

  1. 将所有的类型参数,替换为object(若指定边界,例extend xxx,则xxx来替换),字节码仅包括类,方法,接口。

    例如:ArrayList<Object>ArrayList<String>字节码中一样,JVM视为同一类型。

  2. 类型的擦除不会创建新类,也就不会产生运行时的开销。

五、语法糖

​ 语法糖是指为了方便程序员使用的一种语法结构,在编译期间会被转换为基础的语法结构。其实我们每天都在和语法糖打交道,只是我们自己不了解。下面我们看看在Java中都有哪些语法糖。

Java中的语法糖有很多,我这只举例几个常见的例子。

1. 数据类型

​ Java虽然号称完全面向对象,但是他还是有8种基本类型 + String字符串对象,为了照顾性能。

  1. byte: 字节,8位,最大存储数据量是255,存放的数据范围是-128~127之间。
  2. boolean: 只有true和false两个取值。
  3. char: 16位,2字节,存储Unicode码,用单引号赋值。
  4. short: 16位,2字节,最大数据存储量是65536,数据范围是 -32768~32767 之间
  5. int: 32位,4字节最大数据存储容量是2的32次方减1,数据范围是负的2的31次方到正的2的31次方减1。
  6. float: 32位,数据范围在3 4e 45~1 4e38,直接赋值时必须在数字后加上f或F。
  7. long: 64位,8字节最大数据存储容量是2的64次方减1,数据范围为负的2的63次方到正的2的63次方减1
  8. double: 64位,数据范围在4 .9e-324~1 .8e308,赋值时可以加d或D也可以不加。

2. 泛型

​ 我们知道,处理泛型有两种方法:

  1. Code specialization:在实例化一个泛型类或泛型方法时都产生一份新的目标代码。 C++就是使用这种方式。C++编译器会为每一个泛型类实例生成一份执行代码。执行代码中integer list和string list是两种不同的类型。

  2. Code sharing:对每个泛型类只生成唯一的一份目标代码;Java就是使用这种方式。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除实现的,在编译期就会擦除类型。(相当于在编译后直接将数据类型擦去),但是引用的类型还在。

class ParadigmDemo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
}
}

// 编译后
class ParadigmDemo {
ParadigmDemo() {
}

public static void main(String[] args) {
// 可以看到后面ArrayList的类型已经去掉,但是引用的类型还在。
List<Integer> list = new ArrayList();
list.add(1);
list.add(2);
}
}

​ 所以方法重载是不能用带范型的参数进行区分的,因为编译过后,相当于擦除了new的数据类型,他们的函数签名是相同的,编译器会报错,如下图。

在这里插入图片描述

3. 自动拆箱、装箱

​ Java对于每种基本类型,都有对应的包装类型,来适应面向对象的思想,方便和其他类进行交互。装箱就是int 转化成 Integer 等等。反之就是拆箱。

原始类型:boolean、char、byte、short、int、long、float、double

封装类型:Boolean、Character、Byte、Short、Integer、Long、Float、Double

装箱和拆箱其实是一种语法糖,是Java在编译期自动完成的,并不需要程序员做什么额外的工作。从代码上来讲就是调用valueOf方法。

(1)基本数值—->包装对象

​ 就是包装对象.valueOf(基本数值);

Integer iii = Integer.valueOf(4);//使用包装类中的valueOf方法

(2)包装对象—->基本数值

​ 基本数值 = 包装对象实例.基本数值Value();

int num = i.intValue();

4. foreach

​ 其实我们经常使用的foreach,其实也是一种语法糖。他编译后,底层是用迭代器来实现的。(集合只能通过迭代器实现遍历)例如下面这个例子

class ForDemo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);

for (int i : list){
System.out.println(i);
}
}
}

// 编译后
class ForDemo {
ForDemo() {
}

public static void main(String[] args) {
List<Integer> list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
Iterator var2 = list.iterator();

while(var2.hasNext()) {
int i = (Integer)var2.next();
System.out.println(i);
}
}
}

5. equels与==

​ ==基本变量比较的是值,而引用比较的是地址是否相等

​ equels只能比较引用,默认和==一样比较地址,但一般都会重写来比较引用对象的内容是否相等。

六、反射

1. 过程

​ 类加载过程:当编译一个新类时,会产生一个同名的.class文件,该字节码类加载进入内存生成对应的 Class 对象。类在第一次使用时才动态加载到 JVM 中。

​ 而反射是在编译时不确定那个类被加载了,而是在运行时才加载类信息。

​ 使用 1.`Class.forName(全限定类名) 2. 类名.class 3. 实例对象.getClass()。 这种方式来控制类的加载,该方法会返回一个 Class 对象。

2. 常用API

​ Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:

  • Field :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
  • Method :可以使用 invoke() 方法调用与 Method 对象关联的方法
  • Constructor :可以用 Constructor 的 newInstance() 创建新的对象。

3. 平时使用用途

反射最重要的用途就是开发各种通用框架,很多框架其实都是配置化(例如:Spring 通过XML文件来配置Bean)的,所以为了保证框架的通用性,它们可能需要根据配置文件来加载不同的对象和类,调用不同的方法,这时候就需要用到反射,运行时动态加载需要的对象。

​ 比如我们发请求 /login,那么后端就会去解析XML配置文件,检索对应映射Map中name为login,创建对应的Servlet实例,并用invoke方法来调用被代理对象的方法,这个过程离不开反射。

4. 动态代理

  • JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理
targetObject.getClass().getInterfaces(), new InvokeHandler);```

​ **InvokeHandler必须实现InvocationHandler**

```java
public class ProxyHandle implements InvocationHandler {
// invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/*-------------------------------*/
// 调用invoke方法
ret = method.invoke(targetObject, args);
return ret;
/*-------------------------------*/
}
}

特点:JDK动态代理机制是委托机制,具体说动态实现接口类,在动态生成的实现类里面委托hanlder去调用原始实现类方法,代理类跟被代理类必须实现一样的接口,属于平级关系

  • CGlib动态代理:通过将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。被代理类和代理类是继承关系,所以代理类是可以赋值给被代理类的。
public Object createProxyObject(Object obj) {
this.targetObject = obj;
// setSuperclass通过修改字节码来进行赋值给被代理类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(obj.getClass());
enhancer.setCallback(this);
// proxyObj是代理对象
Object proxyObj = enhancer.create();
return proxyObj;
}

​ 实现MethodInterceptor接口生成方法拦截器,通过拦截器来拦截方法,进行动态代理逻辑。

@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
// 拦截方法
if ("addUser".equals(method.getName())) {
// 检查权限
checkPopedom();
}
Object obj = method.invoke(targetObject, args);
return obj;
}

5. 两种动态代理区别

​ 动态代理指的是,客户通过代理类来调用其他对象的方法,是在程序运行时根据需求动态创建目标类的代理对象。

区别:

  1. JDK代理使用的是反射机制,CGLIB使用的继承机制,通过修改字节码生成子类,代理类是可以赋值给被代理类的
  2. JDK动态代理的方式创建代理对象效率较高,执行效率较低,而cglib创建效率较低,执行效率高。

七、注解

​ Java注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。

1. 原理

​ 注解的本质实际是一个继承了Annotation的特殊接口,其具体的实现类是Java运行时生成的动态代理类,而我们可以通过反射来获取注解,返回代理对象proxy1,通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler 的invoke 方法。该方法会从memberValues 这个Map 中索引出对应的值。而memberValues 的来源是Java 常量池。

2.元注解

@Documented – 注解是否将包含在JavaDoc中
@Retention – 定义该注解的生命周期

● RetentionPolicy.SOURCE : 在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码@Override, @SuppressWarnings都属于这类注解。
● RetentionPolicy.CLASS : 在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式
● RetentionPolicy.RUNTIME : 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。我们自定义的注解通常使用这种方式。

@Target – 注解用于什么地方
@Inherited – 是否允许子类继承该注解

3. 自定义注解

​ 自定义注解类编写的一些规则:

  1. Annotation 型定义为@interface, 所有的Annotation 会自动继承java.lang.Annotation这一接口,并且不能再去继承别的类或是接口.

  2. 参数成员只能用public 或默认(default) 这两个访问权修饰

  3. 参数成员只能用基本类型byte、short、char、int、long、float、double、boolean八种基本数据类型和String、Enum、Class、annotations等数据类型,以及这一些类型的数组.

  4. 要获取类方法和字段的注解信息,必须通过Java的反射技术来获取 Annotation 对象,因为你除此之外也没有别的获取注解对象的方法

  5. 注解也可以没有定义成员,,不过这样注解就没啥用了

    PS:自定义注解需要使用到元注解

@Target(FIELD)
@Retention(RUNTIME) // 生命周期
@Documented // 注解标明
// 类型为@interface
public @interface FruitName {
String value() default "";
}
/*-------------------------注解反射获取举例-------------------------------------*/
// field.getAnnotation(FruitName.class);
Field[] fields = clazz.getDeclaredFields();
for(Field field :fields){
if(field.isAnnotationPresent(FruitName.class)){
FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class);
strFruitName=strFruitName+fruitName.value();
System.out.println(strFruitName);
}
}

八、I/O

1. I/O 分类

  • 磁盘操作:File
  • 字节操作:InputStream 和 OutputStream
  • 字符操作:Reader 和 Writer
  • 对象操作:Serializable
  • 网络操作:Socket
  • 非阻塞的输入/输出:NIO

Java I/O 使用了装饰者模式来实现。 InputStream 为例,

  • InputStream 是抽象组件;
  • FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
  • FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);

2. 对象流

2.1 序列化

序列化就是将一个对象转换成字节序列,方便存储和传输。

  • 序列化:ObjectOutputStream.writeObject()

  • 反序列化:ObjectInputStream.readObject()

    不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。

2.2 Serializable

​ 序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。

private static class A implements Serializable {

private int x;
private String y;

A(int x, String y) {
this.x = x;
this.y = y;
}

@Override
public String toString() {
return "x = " + x + " " + "y = " + y;
}
}

2.3 transient

​ transient 关键字可以使一些属性不会被序列化。

​ ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。

private transient Object[] elementData;

3. NIO

3.1 IO模型

​ IO模型就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。

​ Java共支持3种网络编程模型/IO模式:BIO、NIO、AIO

​ 1) Java BIO : 同步阻塞IO(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不执行会造成不必要的线程开销。

在这里插入图片描述

BIO编程简单流程

  1. 服务器端启动一个ServerSocket

  2. 客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个客户 建立一个线程与之通讯

  3. 客户端发出请求后, 先咨询服务器是否有线程响应,如果没有则会等待(阻塞),或者被拒绝

  4. 如果有响应,客户端线程会等待请求结束后,在继续执行(同步)。

2) Java NIO : 同步非阻塞,NIO是多路复用的,在内核设立了专门的线程selector去轮询访问所有的socket,而不需要具体操作的线程阻塞等待

​ NIO是 面向缓冲区。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。

BIO和NIO的比较:

  1. 效率:BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多

  2. 阻塞:BIO 是阻塞的,NIO 则是非阻塞的

  3. BIO基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道

3) Java AIO(NIO.2 了解即可) : 异步非阻塞,AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用

3.2 BIO、NIO、AIO适用场景

在这里插入图片描述

​ 1)BIO方式适用于连接数目比较小且固定的架构,这种方式当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大,且线程会阻塞,造成线程资源的浪费,JDK1.4以前的唯一选择,但程序简单易理解。

​ 2)NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。

​ 3)AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

3.3 NIO三大核心组件

​ NIO 有三大核心部分:Channel(通道)Buffer(缓冲区), Selector(选择器)

在这里插入图片描述

​ 关系图解读:

1)每个channel 都会对应一个Buffer

2)Selector 对应一个线程, 一个线程对应多个channel(连接)

3)该图反应了有三个channel 注册到 该selector //程序

4)程序切换到哪个channel 是有事件决定的, Event 就是一个重要的概念

5)Selector 会根据不同的事件,在各个通道上切换

6)Buffer 就是一个内存块 , 底层是有一个数组

7)数据的读取写入是通过Buffer, 这个和BIO , BIO 中要么是输入流,或者是输出流, 不能双向,但是NIO的Buffer 是可以读也可以写, 需要 flip 方法切换

8)channel 是双向的, 可以返回底层操作系统的情况, 比如Linux , 底层的操作系统通道就是双向的

3.4 缓冲区Buffer

​ 缓存的本质就是一个可以读写的数据块(底层是个数组),有ByteBuffer,ShortBuffer,CharBuffer,IntBuffer,LongBuffer,DoubleBuffer,FloatBuffer类型对具体的数据类型进行读写,提高了速率。

​ Channel读取和写入数据必须经过Buffer,避免了阻塞,因为Buffer中的数据达到一个规模时才会通过Channel进行读写。

在这里插入图片描述

3.5 通道Channel

​ Channel通道类似于流,但有些区别,并且NIO 还支持 通过多个Buffer (即 Buffer 数组) 完成读写操作。

  • 通道是双向的,可以同时进行读写,而流只能读或者只能写

  • 通道可以从缓冲读数据,也可以写数据到缓冲:

  • 通道可以实现异步读写数据

    常见的API:读写都是针对缓冲区(内存)而言,它进行读写

    1)read(ByteBuffer dst) ,// 从通道读取数据并放到缓冲区中

    2)public int write(ByteBuffer src) // 把缓冲区的数据写到通道中

    3)public long transferFrom(ReadableByteChannel src, long position, long count)// 从目标通道中复制数据到当前通道

    4)public long transferTo(long position, long count, WritableByteChannel target)// 把数据从当前通道复制给目标通道

3.6 选择器Selector

​ NIO使用一个线程,来处理多个客户端Channel请求(提高了并发度),就会用到Selector选择器(又叫多路复用器),多个Channel以事件的方式可以注册到同一个Selector,Selector能够检测多个注册的通道中是否有事件发生。

​ 只有在轮询了注册Channel真正有读写事件发生时,才会进行读写,没有数据可用的话,该线程就会进行其他任务,就大大地减少了系统开销,不用去维护多线程,避免了多线程之间的上下文切换导致的开销。

在这里插入图片描述

3.7 NIO网络编程

​ Selector、SelectionKey、ServerScoketChannel和SocketChannel关系图:

注意:(1)NIO中的 ServerSocketChannel功能类似ServerSocket,SocketChannel功能类似Socket
(2)且ServerSocketChannel和SocketChannel都是注册到Selector上

在这里插入图片描述

  1. 当客户端连接时,会通过ServerSocketChannel 得到 SocketChannel

  2. Selector 进行监听 select 方法, 返回有事件发生的通道的个数

  3. SocketChannel注册到Selector上, register(Selector sel, int ops), 一个selector上可以注册多个SocketChannel

  4. 注册后返回一个 SelectionKey, 会和该Selector关联(存进一个Set集合),可以同SelectionKey来反向获取通道SocketChannel

  5. 还可以得到各个 SelectionKey (有事件发生)

  6. 可以通过得到的 channel , 完成业务处理

3.7.1 SelectionKey

​ SelectionKey,表示 Selector 和Channel的注册关系,因为是SocketChannel注册后返回的对应值。

常见API:

public abstract Selector selector(); //得到与之关联的 Selector 对象
public abstract SelectableChannel channel(); //得到与之关联的通道
public final Object attachment(); //得到与之关联的共享数据
public abstract SelectionKey interestOps(int ops); //设置或改变监听事件
public final boolean isAcceptable(); //是否可以 accept
3.7.2 ServerSocketChannel

​ ServerSocketChannel,在服务器端监听新的客户端Socket通道连接。还可以注册一个选择器Selector并设置监听事件,register(Selector sel, int ops) 。

常见API:

public static ServerSocketChannel open() //得到一个 ServerSocketChannel 通道
public final ServerSocketChannel bind(SocketAddress local) //设置服务器端端口号
public final SelectableChannel configureBlocking(boolean block) //设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
public SocketChannel accept() //接受一个连接,返回代表这个连接的通道对象
public final SelectionKey register(Selector sel, int ops) //注册到选择器并设置监听事件
3.7.3 SocketChannel通道

​ SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。

常见API:

public static SocketChannel open(); //得到一个 SocketChannel 通道
public final SelectableChannel configureBlocking(boolean block); //设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
public boolean connect(SocketAddress remote); //连接服务器
public boolean finishConnect(); //如果上面的方法连接失败,接下来就要通过该方法完成连接操作
public int write(ByteBuffer src); //将缓冲区中数据往通道里写
public int read(ByteBuffer dst); //从通道里读数据到缓冲区
public final SelectionKey register(Selector sel, int ops, Object att); //注册到选择器并设置监听事件,最后一个参数可以设置共享数据
public final void close(); //关闭通道

3.8 零拷贝

​ 在 Java 程序中,常用的零拷贝有 mmap(内存映射)sendFile

注意:零拷贝是指没有CPU拷贝。

​ 在传统的IO和网络编程操作中,要拷贝文件,必然会经过如下过程:(DMA直接内存拷贝不使用CPU)

在这里插入图片描述

​ 需要要文件系统(硬件)到网络协议栈,即需要经过一次DMA拷贝从硬件驱动到内核缓冲,然后再从内核缓冲经过两次CPU拷贝到用户缓冲、socket buffer,最后再DMA拷贝到协议栈传输。

​ 需要进行四次上下文切换,即内核态到用户态的切换。

3.8.1 mmap优化

​ mmap即内存映射,将文件映射到内核缓冲区,同时 用户缓冲和 内核缓冲可以共享数据。这样进行网络传输减少拷贝次数。

​ 内核缓冲可以直接拷贝到socket 缓冲中,减少拷贝次数。

在这里插入图片描述

3.8.2 sendfile优化

​ 数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,不仅减少拷贝的次数,还减少了一次上下文切换。

​ 注意:这里其实有 一次cpu 拷贝 kernel buffer -> socket buffer但是,拷贝的信息很少,比如lenght , offset , 消耗低,可以忽略。

在这里插入图片描述

3.8.3 mmap 和 sendFile 的区别
  1. mmap 需要 4 次上下文切换,3 次数据拷贝,即硬件到内核缓冲,到socket缓冲,到协议栈。

    sendFile 需要 3 次上下文切换,最少 2 次数据拷贝,即硬件到内核缓冲,(内核缓冲到socket buffer,socket buffer 到协议栈)内核缓冲到协议栈。

  2. sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)

  3. mmap 适合小数据量读写,sendFile 适合大文件传输。

补充

1. 序列化

​ 序列化:内存中的数据对象只有转换为二进制流的形式,才能进行网络间的进程传输,以及数据的持久化。

序列化方式:

  1. Java原生的序列化,实现了Serializable接口(标识作用),需要显示的创建SerialVersionUID(根据类的内部实现,类名,方法名,属性等生成),如果不设置则自动生成,这就会使代码修改后,同一个类取值不同
  2. Json序列化:数据对象转换成Json的类型,不需要提供类型信息,所以反序列化时就需要提供类型信息(xxx.class)

注意:类中的字段如若为敏感的信息,不需要序列化,加transient避免序列化。

静态变量也不能序列化,反序列化后可以得到结果是因为其从jvm中获得的,而不是反序列化的结果。

2. JDK与JRE

​ JDK = JRE(运行环境)+ 工具

  • JRE:Java Runtime Environment,Java 运行环境的简称,为 Java 的运行提供了所需的环境。它是一个 JVM 程序,主要包括了 JVM 的标准实现和一些 Java 基本类库。
  • JDK:Java Development Kit,Java 开发工具包,提供了 Java 的开发及运行环境。JDK 是 Java 开发的核心,集成了 JRE 以及一些其它的工具,比如编译 Java 源码的编译器 javac 等。

3. java8新特性

Lambda表达式和Functional接口:Lambda表达式可以允许把函数作为参数传参可以用(类型 参数)->{方法体}替代以前的匿名内部类,方法体中可以调用参数与成员变量和局部变量(隐式转换为final),一般使用参数数组结合forEach或sort等方法时使用,传参也可以不指定类型,参数类型和返回值类型是编译器推测出来的。Functional接口使用@FunctionalInterface注解修饰接口,使得这样的接口可以被隐式转换为lambda表达式。

default修饰符修饰接口中的方法,可以为接口中的方法写默认实现,实现类可以自己决定是否覆盖这个方法

方法引用:直接引用已有Java类或对象(实例)的方法(Class名::method)或构造器(Class名::new)。与lambda联合使用
重复注解

Stream ,函数式编程风格,集合的批处理和流式操作,搭配lambda 表达式进行去重(distinct )、分类(Collectors.groupingBy)、过滤(filter),加和,最大最小等函数操作。

新的Date-Time API :Clock,LocaleDate与LocalTime
jdk1.8中JVM的新特性:抛弃了永久代,数据转移到堆中,引入元空间,元空间作用与永久代类似,都是方法区的实现,元空间不在虚拟机中,而是在本地内存下,字符串不存在元空间,存在字符串常量池里面。

4. 深浅拷贝

浅拷贝中二者的属性中的同一个对象引用指向的是同一个区域,只拷贝了地址引用,clone方法默认为浅拷贝。

深拷贝会把对象引用对应的实际内容在堆中再建一份,并指向,所以深拷贝时,基本数据类型,和对象引用全部不是一样的。