Java基础

💠Java基础

📓java语言特点

  • Java 为纯面向对象的语言。它能够直接反应现实生活中的对象。
  • 具有平台无关性。Java 利用 Java 虚拟机运行字节码,无论是在 Windows、Linux 还是 MacOS 等其它平台对 Java 程序进行编译,编译后的程序可在其它平台运行。
  • Java 为解释型语言,编译器把 Java 代码编译成平台无关的中间代码,然后在 JVM 上解释运行,具有很好的可移植性。
  • Java 提供了很多内置类库。如对多线程支持,对网络通信支持,最重要的一点是提供了垃圾回收器。
  • Java 具有较好的安全性和健壮性。Java 提供了异常处理和垃圾回收机制,去除了 C++中难以理解的指针特性。

📓JDK 与 JRE 有什么区别?

  • JDK:Java 开发工具包(Java Development Kit),提供了 Java 的开发环境和运行环境。
  • JRE:Java 运行环境(Java Runtime Environment),提供了 Java 运行所需的环境。
  • JDK 包含了 JRE。如果只运行 Java 程序,安装 JRE 即可。要编写 Java 程序需安装 JDK。

📓标识符

  • 标识符的含义:在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了 标识符 。简单来说, 标识符就是一个名字
  • 命名规则:(硬性要求) 标识符可以包含英文字母,0-9的数字,$以及_ 标识符不能以数字开头 标 识符不是关键字
  • 命名规范:(非硬性要求) 类名规范:首字符大写,后面每个单词首字母大写(大驼峰式)。 变量 名规范:首字母小写,后面每个单词首字母大写(小驼峰式)。 方法名规范:同变量名。

📓关键字

分类 关键字
访问控制 private protected public
类,方法和变量修饰符 abstract class extends final implements interface native
new static strictfp synchronized transient volatile enum
程序控制 break continue return do while if else
for instanceof switch case default assert
错误处理 try catch throw throws finally
包相关 import package
基本类型 boolean byte char double float int long
short
变量引用 super this void
保留字 goto const

📓数据类型

  • Java中,如果对整数不指定类型,默认时int类型,对小数不指定类型,默认是double类型。
  • 基本类型由小到大,可以自动转换,但是由大到小,则需要强制类型转换。

📙基础数据类型

基本类型 位数 字节 默认值 取值范围 封装类
byte 8 1 0 -128 ~ 127 Byte
short 16 2 0 -32768 ~ 32767 Short
int 32 4 0 -2147483648 ~ 2147483647 Integer
long 64 8 0L -9223372036854775808 ~ 9223372036854775807 Long
char 16 2 ‘\u0000’ 0 ~ 65535 Float
float 32 4 0f 1.4E-45 ~ 3.4028235E38 Double
double 64 8 0d 4.9E-324 ~ 1.7976931348623157E308 Boolean
boolean 1 false true、false Character

📙基础类型的后缀:

long : l 或 L
float: f 或 F;
double: d 或 D

📙隐式转换

📚复合运算符的隐式转换

  • 复合运算符(+=、-=、*=、/=、%=)是可以将右边表达式的类型自动强制转换成左边的类型

📚整形字面常量隐式转换的限制

  • 整形字面常量的大小超出目标类型所能表示的范围时,要手动强制类型转换。

    1
    2
    byte b = 128;//编译错误,128超出byte类型所能表示的范围
    byte c = (byte)128;//编译通过
    JAVA
  • 对于传参数时,必须要显式地进行强制类型转换,明确转换的类型(编译器之所以这样要求,其实为了避免 方法重载出现的隐式转换 与 小类型自动转大类型 发生冲突)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public static void main(String[] args) {
    shortMethod(8);//编译错误
    shortMethod((short)8); //编译通过
    longMethod(8);//编译通过,因为这是小类型变成大类型,是不需要强制类型转换的
    }
    public static void shortMethod(short c){
    System.out.println(c);
    }
    public static void longMethod(short l){
    System.out.println(l);
    }
    JAVA

📚特殊的char类型

char类型是一个无符号类型,所以char类型与其他基本类型不是子集与父集间的关系(其他类型都是有符号的类型)。也就是说,char类型与byte、short之间的转换都需要显式的强制类型转换(小类型自动转换成大类型失败)。

  • char类型与byte、short的相互转换,都需要显式地强类型制转换。
  • 对于数值是负数的,都需要进行显式地强制类型转换,特别是在整形字面常量的隐式转换中。
  • char类型转换成int、long类型是符合 小类型转大类型的规则,即无需要强制类型转换。

📚java的运算结果的类型有两个性质:

  • 运算结果的类型必须是int类型或int类型以上。
  • 最高类型低于int类型的,运算结果都为int类型。否则,运算结果与表达式中最高类型一致。

📙包装类型

基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间, 必须通过实例化开辟数据空间之后才可以赋值。

基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。

所谓包装类,就是能够直接将简单类型的变量表示为一个类,在执行变量类型的相互转换时,我们会大量使用这些包装类。

以下用途:

  1. 作为基本数据类型对应的类类型,提供了一系列实用的对象操作,如类型转换,进制转换等
  2. 集合不允许存放基本数据类型,故常用包装类
  3. 包含了每种基本类型的相关属性,如最大值,最小值,所占位数等

包装类都为final 不可继承
包装类型都继承了Number抽象类

1
2
Integer x = 2;     // 装箱 调用了 Integer.valueOf(2)
int y = x; // 拆箱 调用了 X.intValue()
JAVA

new Integer(123) 与 Integer.valueOf(123) 的区别在于:

  • new Integer(123) 每次都会新建一个对象;
  • Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。

valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容

📙自动拆装箱

装箱就是自动将基本数据类型转换为包装器类型(int–>Integer);调用方法:Integer的 valueOf(int) 方法
拆箱就是自动将包装器类型转换为基本数据类型(Integer–>int)。调用方法:Integer的 intValue方法

  1. 基本型和基本型封装型进行“==”运算符的比较,基本型封装型将会自动拆箱变为基本型后再进行比较
  2. 两个Integer类型进行“==”比较,如果其值在-128至127,那么返回true,否则返回false,
  3. 两个基本型的封装型进行equals()比较,首先equals()会比较类型,如果类型相同,则继续比较值,如果值也相同,返回true
  4. 基本型封装类型调用equals(),但是参数是基本类型,这时候,先会进行自动装箱,基本型转换为其封装类型,再进行比较。

📙缓存池

包装类型内存使用 private static class IntegerCache,声明一个内部使用的缓存池

如Integer中有个静态内部类IntegerCache,里面有个cache[],也就是Integer常量池,常量池的大小为一个字节(-128~127)
为啥把缓存设置为[-128,127]区间?性能和资源之间的权衡。

在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界是可调的,在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax=<size> 来指定这个缓冲池的大小。

基本类型对应的缓冲池如下:

  • boolean values: true and false
  • all byte values
  • short values: between -128 and 127
  • int values: between -128 and 127
  • char: in the range \u0000 to \u007F

📓BigDecimal

BigDecimal 主要用于处理解决精度丢失问题

float和double类型主要是为了科学计算和工程计算而设计的。执行二进制浮点运算,这是为了在广泛的数字范围上提供较为精确的快速近似计算而精心设计的。然而,它们并没有提供完全精确的结果

1
2
3
4
5
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999964
System.out.println(a == b);// false
JAVA

📓字符串

📙String

String 被声明为 final,因此它不可被继承

  • Java 8 中,String 内部使用 char 数组存储数据。
  • Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。

📄String不可变原因:

  1. 保存字符串的数组被 final 修饰且为私有的,并且 String 类没有提供/暴露修改这个字符 串的⽅法。

  2. String 类被 final 修饰导致其不能被继承,进⽽避免了⼦类破坏 String 不可变

对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象

📄不可变的好处

  1. 可以缓存 hash 值
  2. String Pool 的需要。如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
  3. 安全性。String 经常作为参数,String 不可变性可以保证参数不可变。如网络传输
  4. 线程安全

📄关于String使用new创建的问题:

  • String str1 = “aaa”; 是在常量池中获取对象(“aaa” 属于字符串字面量,因此编译时期会在常量池中创建一个字符串对象),

  • String str2 = new String(“aaa”) ; 一共会创建两个字符串对象一个在堆中,一个在常量池中(前提是常量池中还没有 “aaa” 字符串对象)。

📚String类型常量池

String类型的常量池比较特殊。它的主要使用方法有两种:

  • 直接使用双引号声明出来的String对象会直接存储在常量池中。
  • 如果不是用双引号声明的String对象,可以使用 String 提供的 intern 方法。 String.intern() 是一个 Native 方法,它的作用是: 如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用; 如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。

📙StringBuffer

StringBuffer是可变类,对应的字符串的改变不会产生新的对象。

StringBuffer的读写方法都使用了synchronized修饰,同一时间只有一个线程进行操作,所以是线程安全的

📙StringBuilder

StringBuilder是可变类,对应的字符串的改变不会产生新的对象(线程不安全)。

📙三者比较

String、StringBuilderStringBuffer三者的执行效率:
StringBuilder > StringBuffer > String。这个实验结果是相对而言的,不一定在所有情况下都是这样。

比如String str = “hello”+ “world”的效率就比 StringBuilder st = new StringBuilder().append("hello").append("world")要高。

对于三者使用的总结:

  1. 操作少量的数据: 适用String
  2. 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer

📓数组

数组初始化的两种方式:

  • 静态初始化(声明并初始化,此时不能指定容量)

    1
    2
    int[] arr = new int[]{1, 2, 3}
    int[] arr = {1, 2, 3}
    JAVA
  • 动态初始化(先声明再初始化,此时必须指定容量)

    1
    2
    3
    4
    int[] arr = new int[3];
    arr[0] = 1;
    arr[1] = 2;
    arr[2] = 3;
    JAVA

📓变量存储位置

  • 常量池:未经 new 的常量

  • 堆区:成员变量的引用,new 出来的变量

  • 栈区:局部变量的引用

  • 成员变量的引用在堆区,是因为成员变量的所属对象在堆区,所以它也在堆区

  • 局部变量的引用在栈区,是因为局部变量不属于某一个对象,在被调用时才被加载,所以在栈区。

📓Java程序初始化顺序

在 Java 语言中,当实例化对象时,对象所在类的所有成员变量首先要进行初始化,只有当所有类成员完成初始化后,才会调用对象所在类的构造函数创建象。

初始化一般遵循3个原则:

  • 静态对象(变量)优先于非静态对象(变量)初始化,静态对象(变量)只初始化一次,而非静态对象(变量)可能会初始化多次;
  • 父类优先于子类进行初始化;
  • 按照成员变量的定义顺序进行初始化。 即使变量定义散布于方法定义之中,它们依然在任何方法(包括构造函数)被调用之前先初始化;

🔸加载顺序

  • 父类(静态变量、静态语句块)
  • 子类(静态变量、静态语句块)
  • 父类(实例变量、普通语句块)
  • 父类(构造函数)
  • 子类(实例变量、普通语句块)
  • 子类(构造函数)

📓continue、break 和 return 的区别

  1. continue :指跳出当前的这一次循环,继续下一次循环。

  2. break :指跳出整个循环体,继续执行循环下面的语句。

  3. return 用于跳出所在方法,结束该方法的运行。return 一般有两种用法:

    • return; :直接使用 return 结束方法执行,用于没有返回值函数的方法

    • return value; :return 一个特定值,用于有返回值函数的方法

📓instanceof 关键字的作用

instanceof 是 Java 的一个二元操作符,类似于 ==,>,< 等操作符。

instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。

📓final、finally和finalize的区别

📙final 关键字

📚final 类

final 类不能被继承,没有类能够继承 final 类的任何特性。

📚final 方法

类中的 final 方法可以被子类继承,但是不能被子类修改,声明 final 方法的主要目的是防止该方法的内容被修改。

📚final 变量

final变量能被显式地初始化并且只能初始化一次。

  • 修饰引用类型:被声明为 final 的对象的引用不能指向不同的对象。但是 final 对象里的数据可以被改变。也就是说 final 对象的引用不能改变,但是里面的值可以改变。
  • 修饰基础数据类型:final 使数值不变;

📙finally 关键字

在异常处理的时候,提供 finally 块来执行任何的清除操作。如果抛出一个异常,那么相匹配的 catch 字句就会执行,然后控制就会进入 finally 块,前提是有 finally 块。例如:数据库连接关闭操作上

  finally 作为异常处理的一部分,它只能用在 try/catch 语句中,并且附带一个语句块,表示这段语句最终一定会被执行(不管有没有抛出异常),经常被用在需要释放资源的情况下。

📙finalize 关键字

finalize() 是 Object 中的方法,当垃圾回收器将要回收对象所占内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用它 finalize() 方法,让此对象处理它生前的最后事情(这个对象可以趁这个时机挣脱死亡的命运)。要明白这个问题,先看一下虚拟机是如何判断一个对象该死的。

  可以覆盖此方法来实现对其他资源的回收,例如关闭文件。

📓transient关键字

Java 的 transient 关键字,只需要实现 Serilizable 接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。

📓native关键字

native(即 JNI,Java Native Interface),凡是一种语言,都希望是纯。比如解决某一个方案都喜欢就单单这个语言来写即可。Java 平台有个用户和本地 C 代码进行互操作的 API,称为 Java Native Interface (Java本地接口)。

📓static关键字

  • 静态变量

    静态变量在内存中只存在一份,只在类初始化时赋值一次。

  • 静态方法

    静态方法在类加载的时候就存在了,它不依赖于任何实例,所以静态方法必须有实现,也就是说它不能是抽象方法(abstract)。

  • 静态语句块

    静态语句块在类初始化时运行一次。

  • 静态内部类

内部类的一种,静态内部类不依赖外部类,且不能访问外部类的非静态的变量和方法。

  • 静态导包

    1
    import static com.xxx.ClassName.*
    JAVA

📓super关键字

  1. 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。
  2. 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。
  3. 泛型中用于约束泛型的下界。如< ? super Apple>

💠面向对象

📓继承

🏳‍🌈接口没有继承Object类

📄继承规则:

  1. 类与类之间的关系为继承,只能单继承,但可以多层继承。

  2. 类与接口之间的关系为实现,既可以单实现,也可以多实现。

  3. 接口与接口之间的关系为继承,既可以单继承,也可以多继承。

📄关于继承的3个点

  1. ⼦类拥有⽗类对象所有的属性和⽅法(包括私有属性和私有⽅法),但是⽗类中的私有属性和⽅ 法⼦类是⽆法访问,只是拥有。

  2. ⼦类可以拥有⾃⼰属性和⽅法,即⼦类可以对⽗类进⾏扩展。

    1. ⼦类可以⽤⾃⼰的⽅式实现⽗类的⽅法。(以后介绍)。

📓封装

封装是指把⼀个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内 部信息。但是可以提供⼀些可以被外界访问的⽅法来操作属性。

Java 中有三个访问权限修饰符:private、protected 以及 public,

  • 如果不加访问修饰符,表示包级可见。
  • protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。
  • private 仅自己可见
  • public 所有均可见

private 和 protected 不能修饰类。

访问修饰符 同一个类 同包 不同包,子类 不同包,非子类
private
默认
protected
public

📓多态

📄多态的理解(多态的实现方式)

  • 方法重载(overload):实现的是编译时的多态性(也称为前绑定)。
  • 方法重写(override):实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西。

📄面相对象开发方式优点(B65)

  • 较高的开发效率:可以把事物进行抽象,映射为开发的对象。
  • 保证软件的鲁棒性:高重用性,可以重用已有的而且在相关领域经过长期测试的代码。
  • 保证软件的高可维护性:代码的可读性非常好,设计模式也使得代码结构清晰,拓展性好。

📓重载和重写的区别

  • 方法重写(子类与父类之间)

    “两同两小一大”原则

    1. 两同:方法名和参数列表相同
    2. 两小:返回值或声明异常比父类小(或相同)
    3. 一大:访问修饰符比父类的大(或相同)

    细节如下:

    • 参数列表必须完全与被重写方法的相同;
    • 返回类型必须完全与被重写方法的返回类型相同;
    • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected。
    • 父类的成员方法只能被它的子类重写。
    • 声明为final的方法不能被重写。
    • 声明为static的方法不能被重写,但是能够被再次声明。
    • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为private和final的方法。
    • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为public和protected的非final方法。
    • 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
    • 构造方法不能被重写。
    • 如果不能继承一个方法,则不能重写这个方法。
  • 方法重载(同一个类中)

    在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不 同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是 否相同来判断重载。

    • 被重载的方法必须改变参数列表(参数个数或类型或顺序不一样);

    • 被重载的方法可以改变返回类型;

    • 被重载的方法可以改变访问修饰符;

    • 被重载的方法可以声明新的或更广的检查异常;

    • 方法能够在同一个类中或者在一个子类中被重载。无法以返回值类型作为重载函数的区分标准。

📓接⼝和抽象类

📙抽象类

抽象类和抽象方法都使用 abstract 关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。

抽象类和普通类最大的区别是,抽象类不能被实例化,只能被继承。

📙接口

接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。

  • 从 Java 8 开始,接口也可以拥有默认的方法实现
  • 接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。
  • 接口的字段默认都是 static 和 final 的。

📙两者比较

从设计层面上看

  • 抽象类的实现目的,是代码复用,一种模板设计的方式,可以让这些类都派生于一个抽象类。
  • 接口的设计目的,是对类的行为进行约束(更准确的说是一种“有”约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。

从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。

  • 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
  • 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。

设计上对比:

  • 抽象类: 拓展继承该抽象类的模块的类的行为功能(开放闭合原则)
  • 接口:约束继承该接口的类行为(依赖倒置原则)

📓内部类

📙成员内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Outer{//外部类
private int age = 99;
String name = "Coco";
public class Inner{ //内部类
String name = "Jayden";
public void show(){
System.out.println(Outer.this.name);
System.out.println(name);
System.out.println(age);
}
}
public Inner getInnerClass(){
return new Inner();
}
public static void main(String[] args){
Outer o = new Outer();
Inner in = o.new Inner();
in.show();
}
}
JAVA
  1. Inner 类定义在 Outer 类的内部,相当于 Outer 类的一个成员变量的位置,Inner 类可以使用任意访问控制符,如 publicprotectedprivate 等。
  2. Inner 类中定义的 show() 方法可以直接访问 Outer 类中的数据,而不受访问控制符的影响,如直接访问 Outer 类中的私有属性age。
  3. 定义了成员内部类后,必须使用外部类对象来创建内部类对象,而不能直接去 new 一个内部类对象,即:内部类 对象名 = 外部类对象.new 内部类( )。
  4. 编译上面的程序后,会发现产生了两个 .class 文件: Outer.class,Outer$Inner.class{}
  5. 成员内部类中不能存在任何 static 的变量和方法,可以定义常量:
    1. 因为非静态内部类是要依赖于外部类的实例,而静态变量和方法是不依赖于对象的,仅与类相关,简而言之:在加载静态域时,根本没有外部类,所在在非静态内部类中不能定义静态域或方法,编译不通过,非静态内部类的作用域是实例级别。
    2. 常量是在编译器就确定的,放到所谓的常量池了。

:diamonds:温馨提示

  1. 外部类是不能直接使用内部类的成员和方法的,可先创建内部类的对象,然后通过内部类的对象来访问其成员变量和方法。
  2. 如果外部类和内部类具有相同的成员变量或方法,内部类默认访问自己的成员变量或方法,如果要访问外部类的成员变量,可以使用 this 关键字,如:Outer.this.name。

📙静态内部类

static 修饰的内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Outer{//外部类
private int age = 99;
static String name = "Coco";
public static class Inner{//静态内部类
String name = "Jayden";
public void show(){
System.out.println(Outer.name);
System.out.println(name);
}
}
public static void main(String[] args){
Inner i = new Inner();
i.show();
}
}
JAVA
  1. 静态内部类不能直接访问外部类的非静态成员,但可以通过 new 外部类().成员 的方式访问
  2. 如果外部类的静态成员与内部类的成员名称相同,可通过“类名.静态成员”访问外部类的静态成员;
  3. 如果外部类的静态成员与内部类的成员名称不相同,则可通过“成员名”直接调用外部类的静态成员
  4. 创建静态内部类的对象时,不需要外部类的对象,可以直接创建 内部类 对象名 = new 内部类();

📙方法内部类

其作用域仅限于方法内,方法外部无法访问该内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Outer{//外部类
public void Show(){
final int a = 25;
int b = 13;
class Inner{//方法内部类
int c = 2;
public void print(){
System.out.println("访问外部类:" + a);
System.out.println("访问内部类:" + c);
}
}
Inner i = new Inner();
i.print();
}
public static void main(String[] args){
Outer o = new Outer();
o.show();
}
}

JAVA
  1. 局部内部类就像是方法里面的一个局部变量一样,是不能有 public、protected、private 以及 static 修饰符的
  2. 只能访问方法中定义的 final 类型的局部变量,因为:
    • 当方法被调用运行完毕之后,局部变量就已消亡了。但内部类对象可能还存在,直到没有被引用时才会消亡。此时就会出现一种情况,就是内部类要访问一个不存在的局部变量。
    • 使用final修饰符不仅会保持对象的引用不会改变,而且编译器还会持续维护这个对象在回调方法中的生命周期,局部内部类并不是直接调用方法传进来的参数,而是内部类将传进来的参数通过自己的构造器备份到了自己的内部,自己内部的方法调用的实际是自己的属性而不是外部类方法的参数,防止被篡改数据,而导致内部类得到的值不一致

📙匿名内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  public class OuterClass {
public InnerClass getInnerClass(final int num,String str2){
return new InnerClass(){ //匿名内部类
int number = num + 3;
public int getNumber(){//实现抽象方法
return number;
}
}; /* 注意:分号不能省 */
}
public static void main(String[] args) {
OuterClass out = new OuterClass();
InnerClass inner = out.getInnerClass(2, "chenssy");
System.out.println(inner.getNumber());
}
}
interface InnerClass { //匿名内部类要实现的接口
int getNumber();
}
}
JAVA
  1. 使用匿名内部类时,我们必须是继承一个类或者实现一个接口,但是两者不可兼得,同时也只能继承一个类或者实现一个接口。

  2. 匿名内部类中是不能定义构造函数的。

  3. 匿名内部类中不能存在任何的静态成员变量和静态方法。

  4. 匿名内部类为局部内部类,所以局部内部类的所有限制同样对匿名内部类生效。

  5. 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。

📓类关系

👻类与类之间有三种关系:

(1)is-a 包括了继承(类)和实现(接口)关系;

(2)has-a包括了关联、聚合、组合关系;

(3)use-a包括了依赖关系;

注:依赖关系 > 关联关系 > 聚合关系 > 组合关系

📓equals与==的区别

  • 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
  • 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。

细节:

== 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是 否是指相同一个对象。比较的是真正意义上的指针操作。

equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所 以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object 中的equals方法返回的却是==的判断。

📓Hashcode()

📙Hashcode()的作用

hashCode() 返回哈希值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价,这是因为计算哈希值具有随机性,两个值不同的对象可能计算出相同的哈希值。

**hashCode()**:hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值.

  • 对象按照自己不同的特征尽量的有不同的哈希码,作用是用于快速查找
  • 另一个应用就是hash集合的使用

📙为什么hashCode()和equals()方法要一起重写

因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals ⽅法判断两个对象是相等 的,那这两个对象的 hashCode 值也要相等。如果重写 equals() 时没有重写 hashCode() ⽅法的话就可能会导致 equals ⽅法判断是相等的两个 对象, hashCode 值却不相等。

📓java复制

对于基本类型,直接赋值复制,对于对象类型分为浅拷贝与深拷贝

  1. 浅拷贝:对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
  2. 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

深拷贝的另一种方式,使用序列化和反序列化,获取一个新对象。

📓序列化

定义:🏷序列化:对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建

🏷反序列化:客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

📓枚举

枚举类比较使用==,同样也可以使用equals方法,Enum类中重写了equals实际上还是调用==方法。

1
2
3
4
5
6
7
8
9
10
11
/**
* Returns true if the specified object is equal to this
* enum constant.
*
* @param other the object to be compared for equality with this object.
* @return true if the specified object is equal to this
* enum constant.
*/
public final boolean equals(Object other) {
return this==other;
}
JAVA

📚为什么使用==比较?

因为枚举类在jvm编译成class文件后,实际编译成使用final 修饰的class,final修饰就意味着实例化后不可修改,且都指向堆中的同一个对象

普通的一个枚举类

1
2
3
public enum t {
SPRING,SUMMER,AUTUMN,WINTER;
}
JAVA

反编译后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public final class T extends Enum
{
private T(String s, int i)
{
super(s, i);
}
public static T[] values()
{
T at[];
int i;
T at1[];
System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
return at1;
}

public static T valueOf(String s)
{
return (T)Enum.valueOf(demo/T, s);
}

public static final T SPRING;
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T ENUM$VALUES[];
static
{
SPRING = new T("SPRING", 0);
SUMMER = new T("SUMMER", 1);
AUTUMN = new T("AUTUMN", 2);
WINTER = new T("WINTER", 3);
ENUM$VALUES = (new T[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}
}
JAVA

📓IO流

📙分类

📃Java 的 I/O 大概可以分成以下几类:

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

磁盘操作🌰

File 类可以用于表示文件和目录的信息,但是它不表示文件的内容。递归地输出一个目录下所有文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void listAllFiles(File dir)
{
if (dir == null || !dir.exists()) {
return;
}
if (dir.isFile()) {
System.out.println(dir.getName());
return;
}
for (File file : dir.listFiles()) {
listAllFiles(file);
}
}
JAVA

字节操作🌰

使用字节流操作进行文件复制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void copyFile(String src, String dist) throws IOException
{
FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dist);
byte[] buffer = new byte[20 * 1024];
// read() 最多读取 buffer.length 个字节
// 返回的是实际读取的个数
// 返回 -1 的时候表示读到 eof,即文件尾
while (in.read(buffer, 0, buffer.length) != -1) {
out.write(buffer);
}
in.close();
out.close();
}
JAVA

字符操作🌰

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void readFileContent(String filePath) throws IOException
{
FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
// 装饰者模式使得 BufferedReader 组合了一个 Reader 对象
// 在调用 BufferedReader 的 close() 方法时会去调用 fileReader 的 close() 方法
// 因此只要一个 close() 调用即可
bufferedReader.close();
}
JAVA

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

📖InputStream 是抽象组件;

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

📖InputStream的作用是用来表示那些从不同数据源产生输入的类。

  1. 字节数组
  2. String对象
  3. 文件
  4. “管道“,工作方式与实际管道类似,即一端输入另一端输出
  5. 其他数据源,如Internet连接等

📖Reader 与 Writer

  • 不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
  • InputStreamReader 实现从字节流解码成字符流;
  • OutputStreamWriter 实现字符流编码成为字节流。

📙编码与解码

编码就是把字符转换为字节,而解码是把字节重新组合成字符。如果编码和解码过程使用不同的编码方式那么就出现了乱码。

  • GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
  • UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
  • UTF-16be 编码中,中文字符和英文字符都占 2 个字节。

String 编码转换

1
2
3
4
String str1 = "中文";
byte[] bytes = str1.getBytes("UTF-8");
String str2 = new String(bytes, "UTF-8");
System.out.println(str2);
JAVA

📓操作系统中的IO

📙常见I/O模型对比

所有的系统I/O都分为两个阶段:等待就绪和操作。

举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。

需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”; 而真正的读写操作的阻塞是使用CPU的,真正在”干活”,而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。

📙BIO

传统的BIO中,read去读取网络的数据时,是无法预知对方是否已经发送数据的。因此在收到数据之前,能做的只有等待,直到对方把数据发过来,或者等到网络超时。

📙NIO

NIO模式下,系统调用read,如果发现没数据已经到达,就会立刻返回-1。使用轮询的方式,不断的尝试有没有数据到达。没有得到数据就等一小会再试继续轮询。

NIO解决了线程阻塞的问题 ,但是会带来两个新问题:

  1. 如果有IO连接都要检查,那么就得一个一个的read。这会带来大量的线程上下文切换(read是系统调用,每调用一次就得在用户态和核心态切换一次)
  2. 轮询的休息等待时间无法确定。这里是要猜多久之后数据才能到。等待时间设的太长,程序响应延迟就过大;设的太短,就会造成过于频繁的重试,干耗CPU而已。

📙IO复用模型

定义:多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互,告诉操作系统要监视这些IO是否有事件发生。阻塞读取操作系统epoll方法回调的通知消息。

特点及相关说明:

  • IO多路复用是要和NIO一起使用的。尽管在操作系统级别,NIO和IO多路复用是两个相对独立的事情。也可以只用IO多路复用 + BIO,这时效果还是当前线程被卡住,没有达到IO多路复用的通知请求到来的效果。
  • IO多路复用说的是多个Socket或IO连接,只不过操作系统是一起监听他们的事件而已。

多个数据流共享同一个TCP连接的场景的确是有,比如Http2 Multiplexing就是指Http2通讯中多个逻辑的数据流共享同一个TCP连接。但这与IO多路复用是完全不同的问题。

  • IO多路复用的关键API调用(select,poll,epoll_wait)总是Block的
  • IO多路复用和NIO一起仅仅是解决了调度的问题,避免CPU在这个过程中的浪费,使系统的瓶颈更容易触达到网络带宽,而非CPU或者内存。要提高IO吞吐,还是提高硬件的容量(例如,用支持更大带宽的网线、网卡和交换机)和依靠并发传输(例如HDFS的数据多副本并发传输)。

📚epoll

操作系统级别提供了一些接口来支持IO多路复用,最早的是select、poll,其后epoll是Linux下的IO多路复用的实现。

  • select接口最早实现存在需要调用多次、线程不安全以及限制只能监视1024个链接的问题
  • poll接口修复了select函数的一些问题,但是依然不是线程安全的。
  • epoll接口修复了上述的问题,并且线程安全,会通知具体哪个连接有新数据。
    • epoll通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知(不再需要遍历文件描述符,通过监听回调的机制,也是epoll的魅力)

📚水平触发与边缘触发

epoll除了性能优势,还有一个优点——同时支持水平触发(Level Trigger)和边沿触发(Edge Trigger)。

  • 水平触发只关心文件描述符中是否还有没完成处理的数据,如果有,不管怎样epoll_wait,总是会被返回。简单说——水平触发代表了一种“状态”。
  • 边沿触发只关心文件描述符是否有新的事件产生,如果有,则返回;如果返回过一次,不管程序是否处理了,只要没有新的事件产生,epoll_wait不会再认为这个fd被“触发”了。简单说——边沿触发代表了一个“事件”。

边沿触发把如何处理数据的控制权完全交给了开发者,提供了巨大的灵活性。比如,读取一个http的请求,开发者可以决定只读取http中的headers数据就停下来。在边沿触发下,开发者有机会更精细的定制这里的控制逻辑。

📙信号驱动IO

在通道中安装一个信号器:映射到Linux操作系统中,这就是信号驱动IO。应用进程在读取文件时通知内核,如果某个 socket 的某个事件发生时,请向我发一个信号。

阻塞IO模型、非阻塞IO模型、IO复用模型和信号驱动IO模型都是同步的IO模型。原因是因为,无论以上那种模型,真正的数据拷贝过程,都是同步进行的。

📚AIO

用了AIO可以废弃select,poll,epoll。 linux的AIO的实现方式是内核和应用共享一片内存区域,应用通过检测这个内存区域(避免调用nonblocking的read、write函数来测试是否来数据,因为即便调用nonblocking的read和write由于进程要切换用户态和内核态,仍旧效率不高)来得知fd是否有数据,可是检测内存区域毕竟不是实时的,你需要在线程里构造一个监控内存的循环,设置sleep,总的效率不如epoll这样的实时通知。

📙相关资料

📓Java 中的网络支持

基本概念

  • InetAddress:用于表示网络上的硬件资源,即 IP 地址;
  • URL:统一资源定位符;
  • Sockets:使用 TCP 协议实现网络通信;f
  • Datagram:使用 UDP 协议实现网络通信。

📓java BIO(Blocking IO 阻塞)

  • 在不考虑多线程的情况下,BIO是无法处理多个客户端请求的。
  • BIO 通信模型 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。
  • 多线程情况下对于服务端,服务端只能用线程开启多个线程与客户端建立连接。

BIO多线程情况下的缺点:内存消耗、线程上下文切换

  1. 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
  2. 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
  3. 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU 使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态。
  4. 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。

📓java NIO(Non-blocking/New I/O)

NIO的主要事件有几个:读就绪、写就绪、有新连接到来。

  1. 首先需要注册当这几个事件到来的时候所对应的处理器,事件处理完毕移除SelectKey,若未移除,selector不会检查这些key是否有事件到来。
  2. 然后在合适的时机告诉事件选择器:对这个事件感兴趣。
  3. 其次,用一个死循环选择就绪的事件,会执行系统调用,还会阻塞的等待新事件的到来。(系统调用指的是操作系统的函数调用,Linux 2.6之前是select、poll,2.6之后是epoll,Windows是IOCP)
  4. 新事件到来的时候,会在selector上注册标记位,标示可读、可写或者有连接到来。
  5. 注意,select是阻塞的,无论是通过操作系统的通知(epoll)还是不停的轮询(select,poll),这个函数是阻塞的。

select会进行系统调用(Linux 2.6之前是select、poll,2.6之后是epoll,Windows是IOCP),还会阻塞的等待新事件的到来。新事件到来的时候,会在selector上注册标记位,标示可读、可写或者有连接到来。

  • NIO由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程。

📙零拷贝

IO的拷贝:

  1. 硬件(网卡)拷贝内核缓冲区 (读)
  2. 内核缓冲区拷贝到用户缓冲区 (读)
  3. 用户空间再拷贝到内核空间中的Socket buffer/Write buffer中。(写)
  4. 最后再从Socket buffer中拷贝到网卡缓冲区/硬件资源中。(写)

零拷贝的实现: 使用直接内存,在内核缓冲区中开辟一块用户空间和内核空间共享的直接内存区域,减少了用户缓冲区的复制操作。

📙事件驱动模型

Reactor模式首先是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。

Java的Selector对于Linux系统来说,有一个致命限制:同一个channel的select不能被并发的调用。因此,如果有多个I/O线程,必须保证:一个socket只能属于一个IoThread,而一个IoThread可以管理多个socket。另外连接的处理和读写的处理通常可以选择分开,这样对于海量连接的注册和读写就可以分发。虽然read()和write()是比较高效无阻塞的函数,但毕竟会占用CPU,如果面对更高的并发则无能为力。

avatar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface ChannelHandler{
void channelReadable(Channel channel);
void channelWritable(Channel channel);
}
class Channel{
Socket socket;
Event event;//读,写或者连接
}

//IO线程主循环:
class IoThread extends Thread{
public void run(){
Channel channel;
while(channel=Selector.select()){//选择就绪的事件和对应的连接
if(channel.event==accept){
registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器
}
if(channel.event==write){
getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件
}
if(channel.event==read){
getChannelHandler(channel).channelReadable(channel);//如果可以读,则执行读事件
}
}
}
Map<Channel,ChannelHandler> handlerMap;//所有channel的对应事件处理器
JAVA

相关资料:

📙NIO与BIO区别

  • 通讯方式:NIO 通过Channel(通道) 进行读写,通道是双向的,可读也可写。而BIO使用的流读写是单向的。
  • BIO流是阻塞的,NIO流是不阻塞的。
  • BIO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。
    1. 在面向流的I/O中·可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。
    2. 在NIO厍中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。

NIO 带来了什么

  • 避免多线程
  • 非阻塞I/O,I/O读写不再阻塞,而是返回0
  • 单线程处理多任务
  • 基于block的传输,通常比基于流的传输更高效
  • 更高级的IO函数,zero-copy
  • 事件驱动模型
  • IO多路复用大大提高了Java网络应用的可伸缩性和实用性

Proactor与Reactor

在Reactor中实现读

  1. 注册读就绪事件和相应的事件处理器。
  2. 事件分发器等待事件。
  3. 事件到来,激活分发器,分发器调用事件对应的处理器。
  4. 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。

在Proactor中实现读:

  1. 处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。
  2. 事件分发器等待操作完成事件。
  3. 在分发器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分发器读操作完成。
  4. 事件分发器呼唤处理器。
  5. 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分发器。
  • 两者也有相同点:事件分发器负责提交IO操作(异步)、查询设备是否可操作(同步),然后当条件满足时,就回调handler;
  • 不同点在于,异步情况下(Proactor),当回调handler时,表示I/O操作已经完成;同步情况下(Reactor),回调handler时,表示I/O设备可以进行某个操作(can read 或 can write)。

📙RMI 远程方法调用

java支持,最早的远程调用,使用Remote接口,同时实现类别需要继承UnicastRemoteObject

通过Registry,注册发现远程方法,并调用接口。

📙netty

Netty 是一个 基于 NIO 的 client-server(客户端服务器)框架,使用它可以快速简单地开发网络应用程序。

  • 它极大地简化并优化了 TCP 和 UDP 套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好。
  • 支持多种协议 如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协议。

支持多个交互模型 avatar

  • Reactor分成两部分,mainReactor负责监听server socket,accept新连接;并将建立的socket分派给subReactor。subReactor负责多路分离已连接的socket,读写网络数据,对业务处理功能,其扔给worker线程池完成。

Netty中的事件分为Inbond事件和Outbound事件。

  • Inbound事件通常由I/O线程触发,如TCP链路建立事件、链路关闭事件、读事件、异常通知事件等。
  • Outbound事件通常是用户主动发起的网络I/O操作,如用户发起的连接操作、绑定操作、消息发送等。

相比NIO :

  • NIO在面对断连重连、包丢失、粘包等问题时处理过程非常复杂。Netty的出现正是为了解决这些问题。
  • 解决了JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100%
  • 通过代码封装,简化了服务端与客户端的代码交互。
  • 数据直接复制到directBuffer的工作缓冲区

📓泛型

📙简介

泛型的本质是参数化类型,也就是所操作的数据类型被指定为一个参数。
在集合中存储对象并在使用前进行类型转换是不方便的。泛型防止了那种情况的发生。它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。

📚使用T, E or K,V等被广泛认可的类型占位符。

📖泛型有三种常用的使用方式:泛型类,泛型接口和泛型方法。

📄限定通配符和非限定通配符:

  1. 非限定通配符:<?>表示了非限定通配符,因为<?>可以用任意类型来替代。
  2. 限定通配符:一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界 ,另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界
  3. 泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。

📙类型擦除

📚类型擦除: Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。

  • 如在代码中定义List<Object>List<String>等类型,在编译后都会变成List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到的。
  • 类型擦除后保留的原始类型,最后在字节码中的类型变量变成真正类型。无论何时定义一个泛型,相应的原始类型都会被自动提供,无限定的变量用Object替换。

泛型擦除的例子: 本应该只能储存Integer,在通过反射调用方法时,却可以添加String数据

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) throws Exception {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
list.getClass().getMethod("add", Object.class).invoke(list, "asd");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
// output
//1
//asd
JAVA

📚类型擦除后保留的原始类型:在调用泛型方法时,可以指定泛型,也可以不指定泛型。

  • 在不指定泛型的情况下,泛型变量的类型为该方法中的几种类型的同一父类的最小级,直到Object
  • 在指定泛型的情况下,该方法的几种类型必须是该泛型的实例的类型或者其子类
1
2
3
4
5
6
7
Number f = Test.add(1, 1.2); //这两个参数一个是Integer,另一个是Float,所以取同一父类的最小级,为Number  
Object o = Test.add(1, "asd"); //这两个参数一个是Integer,另一个是String,所以取同一父类的最小级,为Object

//这是一个简单的泛型方法
public static <T> T add(T x,T y){
return y;
}
JAVA

Java不能实现真正的泛型,只能使用类型擦除来实现伪泛型,这样虽然不会有类型膨胀问题,但是也引起来许多新问题

📚PECS原则
如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)

如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;

(Consumer Super) 如果既要存又要取,那么就不要使用任何通配符。

参考:

📓反射

👻先看看知乎回答

 反射是什么呢?当我们的程序在运行时,需要动态的加载一些类这些类可能之前用不到所以不用加载到 JVM,而是在运行时根据需要才加载,这样的好处对于服务器来说不言而喻。

  举个例子我们的项目底层有时是用 mysql,有时用 oracle,需要动态地根据实际情况加载驱动类,这个时候反射就有用了,假设 com.java.dbtest.myqlConnection,com.java.dbtest.oracleConnection 这两个类我们要用,这时候我们的程序就写得比较动态化,通过 Class tc = Class.forName(“com.java.dbtest.TestConnection”); 通过类的全类名让 JVM 在服务器中找到并加载这个类,而如果是 Oracle 则传入的参数就变成另一个了。这时候就可以看到反射的好处了,这个动态性就体现出 Java 的特性了!

  举多个例子,大家如果接触过 spring,会发现当你配置各种各样的 bean 时,是以配置文件的形式配置的,你需要用到哪些 bean 就配哪些,spring 容器就会根据你的需求去动态加载,你的程序就能健壮地运行。


📄反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。

  1. 当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
  2. 类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。
  3. 也可以使用 Class.forName(“com.mysql.jdbc.Driver”) 这种方式来控制类的加载,该方法会返回一个 Class 对象。

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

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

📄反射的优点

  • 可扩展性:应用程序可以利用全限定名创建可扩展对象的实例,如com.demo.Test。
  • 调试器和测试工具: 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。
  • 开发工具:如IDEA开发工具可以从反射中获取类的信息,帮助开发人员代码编写。

📄反射的缺点:如果一个功能可以不用反射完成,那么最好就不用。

  • 性能开销:反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。
  • 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行。
  • 内部暴露:反射破坏了封装性,可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性

📄反射的功能

  • 在运行时判断任意一个对象所属的类

  • 在运行时构造任意一个类的对象

  • 在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用 private 方法)

  • 在运行时调用任意一个对象的方法

重点:是运行时而不是编译时

📄获得Class对象

  • 调用运行时类本身的 .class 属性

    1
    2
    Class clazz1 = Person.class;
    System.out.println(clazz1.getName());
    JAVA
  • 通过运行时类的对象获取 getClass()

    1
    2
    3
    Person p = new Person();
    Class clazz3 = p.getClass();
    System.out.println(clazz3.getName());
    JAVA
  • 使用 Class 类的 forName 静态方法

    1
    2
    3
    public static Class<?> forName(String className)
    // 在JDBC开发中常用此方法加载数据库驱动:
    Class.forName(driver);
    JAVA
  • (了解)通过类的加载器 ClassLoader

    1
    2
    3
    ClassLoader classLoader = this.getClass().getClassLoader();
    Class clazz5 = classLoader.loadClass(className);
    System.out.println(clazz5.getName());
    JAVA

📓异常与错误

📃Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: Error 和 Exception。

其中 Error 用来表示 JVM 无法处理的错误,

📃Exception 分为两种:

  1. 受检异常 :要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。
  2. 非受检异常 :是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复
    • 运行时异常(runtime exception)
    • 错误(Error)

📃RuntimeException是一种Unchecked Exception,即表示编译器不会检查程序是否对RuntimeException作了处理,在程序中不必捕获RuntimException类型的异常,也不必在方法体声明抛出RuntimeException类。一般来说,RuntimeException发生的时候,表示程序中出现了编程错误,所以应该找出错误修改程序,而不是去捕获RuntimeException。

常见RuntimeException异常:NullPointException、ClassCastException、IllegalArgumentException、IndexOutOfBoundException

📃try语句return问题:如果try语句里有return,返回的是try语句块中变量值。详细执行过程如下:

  1. 如果有返回值,就把返回值保存到局部变量中;
  2. 执行jsr指令跳到finally语句里执行;
  3. 执行完finally语句后,返回之前保存在局部变量表里的值。
  4. 针对对象引用的返回,如果finally中有修改值,返回的是引用的对象。 如果try,finally语句里均有return,忽略try的return,而使用finally的return.

📃catch和finally语句不能同时省略!

  • 用try-catch 捕获异常;
  • 用try-finally 清除异常;
  • 用try-catch-finally 处理所有的异常. 三者选一种即可

📃Throwable 类常⽤⽅法

  • String getMessage() : 返回异常发⽣时的简要描述
  • String toString() : 返回异常发⽣时的详细信息
  • String getLocalizedMessage() : 返回异常对象的本地化信息。使⽤ Throwable 的⼦类覆盖这个⽅ 法,可以⽣成本地化信息。如果⼦类没有覆盖该⽅法,则该⽅法返回的信息与 getMessage() 返 回的结果相同
  • void printStackTrace() : 在控制台上打印 Throwable 对象封装的异常信息

📓注解

📙什么是注解

 Annontation 是 Java5 开始引入的新特征,中文名称叫注解。它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。为程序的元素(类、方法、成员变量)加上更直观更明了的说明,这些说明信息是与程序的业务逻辑无关,并且供指定的工具或框架使用。Annontation 像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。

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

  简单来说:注解其实就是代码中的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相对应的处理

📙为什么要注解

传统的方式,我们是通过配置文件 .xml 来告诉类是如何运行的。

有了注解技术以后,我们就可以通过注解告诉类如何运行

例如:我们以前编写 Servlet 的时候,需要在 web.xml 文件配置具体的信息。我们使用了注解以后,可以直接在 Servlet 源代码上,增加注解…Servlet 就被配置到 Tomcat 上了。也就是说,注解可以给类、方法上注入信息。

明显地可以看出,这样是非常直观的,并且 Servlet 规范是推崇这种配置方式的。

📙内置注解

Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中。

🏷作用在代码的注解:

  • @Override: 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated: 标记过时方法。如果使用该方法,会报编译警告。
  • @SuppressWarnings: 指示编译器去忽略注解中声明的警告。

🏷作用在其他注解的注解(或者说元注解)是:

  • @Retention : 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
  • @Documented: 标记这些注解是否包含在用户文档中。
  • @Target: 标记这个注解应该是哪种 Java 成员。
  • @Inherited: 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)

🏷从 Java 7 开始,额外添加了 3 个注解:

  • @SafeVarargs: Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
  • @FunctionalInterface: Java 8 开始支持,标识一个匿名函数或函数式接口。
  • @Repeatable: Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

📙元注解

java.lang.annotation 提供了四种元注解,专门注解其他的注解(在自定义注解的时候,需要使用到元注解):

  1. @Documented:注解是否将包含在JavaDoc中

  2. @Retention(什么时候使用该注解)

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

    • ElementType.CONSTRUCTOR: 用于描述构造器
    • ElementType.FIELD: 成员变量、对象、属性(包括enum实例)
    • ElementType.LOCAL_VARIABLE: 用于描述局部变量
    • ElementType.METHOD: 用于描述方法
    • ElementType.PACKAGE: 用于描述包
    • ElementType.PARAMETER: 用于描述参数
    • ElementType.TYPE: 用于描述类、接口(包括注解类型) 或enum声明 常见的@Component、@Service
  4. @Inherited(是否允许子类继承该注解)

    @Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的annotation 类型被用于一个class,则这个annotation 将被用于该class 的子类

📙自定义注解

  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:自定义注解需要使用到元注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* 水果名称注解
*/
@Target(FIELD)
@Retention(RUNTIME)
@Documented
public @interface FruitName {
String value() default "";//属性写成类似方法
}

JAVA

📓网络编程

📙网络编程基础

  • Java网络编程API建立在Socket基础之上
  • Java网络接口支持IP以上的所有高层协议

📃java网络通信类:

  • InetAddress:用于表示网络上的硬件资源,即 IP 地址;
  • URL:统一资源定位符;
  • Sockets:使用 TCP 协议实现网络通信;
  • Datagram:使用 UDP 协议实现网络通信。

📙Socket

👻Socket,又称套接字,是在不同的进程间进行网络通讯的一种协议、约定或者说是规范。

对于Socket编程,它更多的时候是基于TCP/UDP等协议做的一层封装或者说抽象,是一套系统所提供的用于进行网络通信相关编程的接口。

📃Socket通信伪代码:

  1. 服务器绑定端口:server = new ServerSocket(PORT)
  2. 服务器阻塞监听:socket = server.accept()
  3. 服务器开启线程:new Thread(Handle handle)
  4. 服务器读写数据:BufferedReader PrintWriter
  5. 客户端绑定IP和PORT:new Socket(IP_ADDRESS, PORT)
  6. 客户端传输接收数据:BufferedReader PrintWriter

Socket的特点

  1. Socket基于TCP链接,数据传输有保障
  2. Socket适用于建立长时间链接
  3. Socket编程通常应用于即时通讯

Java基础
http://example.com/2022/09/04/Java/
作者
liziyuan
发布于
2022年9月4日
许可协议