数组与广义表

1 数组的概念、多维数组的实现

1)数组的概念

  • 数组的特点:元素数目固定;下标有界。
  • 数组的操作:按照下标进行读写。

2)多维数组的实现

行优先顺序

存储时先按行从小到大的顺序存储,在每一行中按列号从小到大存储。

列优先顺序

存储时先按列从小到大的顺序存储,在每一列中按行号从小到大存储。

2 矩阵的压缩存储

矩阵的压缩存储就是存储数组时,尽量减少存储空间,但数组中每个元素必须存储。

在矩阵中,如果有规律可寻,只要存储其中一部分,而另外一部分的存储地址可以通过相应的算法将它计算出来,从而占有较少的存储空间达到存储整个矩阵的目的。

矩阵的压缩存储仅能针对特殊矩阵使用,对于没有规律可循的二维数组则不能使用。

1)对称矩阵

只需对对称矩阵中n(n+1)/2个元素进行储存表示

2)三角矩阵

以主对角线划分,三角矩阵有上三角和下三角两种。上三角矩阵它的下三角中的元素均为常数。下三角矩阵正好相反,它的主对角线上方均为常数。

3)稀疏矩阵

if 一个 m * n 的矩阵含有 t 个非零元素,且 t 远远小于 m * n,则称这个矩阵为稀疏矩阵

除了记录非零元素的值之外,还必须同时几下它所在的行和列的位置。稀疏矩阵的存储方法一般有三种:三元组法、行逻辑连接顺序表和十字链表法。

三元组法

用三项内容表示稀疏矩阵中的每个非零元素,形式为:(i,j,value)。
其中,i 表示行序号,j 表示列序号,value 表示非零元素的值

3 广义表的基本概念

1)定义

  • 广义表:是线性表的扩展,具体定义为n(n≥0)个元素的有限集合。
    n的值是广义表的长度,如果n=0称广义表为空表。
  • 长度:广义表中含有元素的个数称
  • 深度:广义表中含有的括号对数
数据元素

广义表的数据元素有两种类型:一个是不可再分的元素(原子元素);一个是可以再分的元素(子表)。

  • 如果所有的元素都是原子元素,则称为线性表。
  • 如果数据元素中含有子表元素,则称为广义表。
记法

广义表一般记作:LS=(a1,a2,……,an)

常见的广义表为:A=()、B=(())、C=(a,b)、D=(A,B,C)、E=(a,E)

特点

广义表有三个重要的特点:

  • 第一:广义表的元素可以是子表,而子表的元素还可以是子表,广义表是一个多层次的结构。
  • 第二:广义表可以为其他广义表所共享。
  • 第三:广义表可以是一个递归表,即表也可以是其本身的一个子表。

2)存储方式

广义表的存储方法有很多种,一般采用链表存储。

flag表示标志位。当flag为0时,表示该结点为原子元素,info表示原子元素的值;当flag为1时表示该结点为子表,info表示指针,指向该子表的第一个结点。 link表示指针,指向广义表的下一个元素。


修饰符

1 访问修饰符

  • private:仅对本类可见
  • public:对所有类可见
    • 接口里方法默认 public
  • protected:对本包和所有子类可见
  • default:对本包可见

一个方法可以访问所属类的所有对象的私有对象

2 非访问修饰符

2.1 final

  • final 域(常量)
    • final 域必须显式指定初始值,赋值后,不能被重新赋值。
    • 基本类型:final 使数值不变
    • 引用类型:final 使引用不变,但是被引用的对象本身是可以修改的
  • final 方法
    • final 方法可以被子类继承,但是不能被子类修改。
    • private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。
  • final 类
    • final 类不能被继承

2.2 static

  • 静态域(变量)
    • 类变量,多实例也只有一份拷贝
  • 静态常量
    • 多与 final 组成静态常量,一般大写
  • 静态方法
    • 类方法,独立于对象,没有隐式参数 this 和 super,可以访问静态域
    • 不可从一个 static 方法内部发出对非 static 方法的调用
    • 必须有实现,不能是抽象方法
  • 静态内部类
    • 只有内部类能被声明为 static。
    • 静态内部类不能访问外部类的非静态的变量和方法。
  • 静态代码块
    • 静态块在类加载时自动执行
  • 静态导包
    • JDK 1.5,用 import static 代替 import 静态导入,可以直接使用类的静态方法和静态域,而不需要使用类名作为前缀。

存在继承的情况下,初始化顺序为:

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

2.3 abstract

  • 抽象类
    • 抽象类不能实例化,唯一目的是为了对该类进行扩充。
    • 抽象类可以不包含抽象方法。
  • 抽象方法
    • 抽象方法是一种没有任何实现的方法,具体实现由子类提供。
    • 任何继承抽象类的子类必须实现父类的所有抽象方法,除非该子类也是抽象类。
    • 包含抽象方法的类,一定要声明为抽象类。

2.4 synchronized

  • 方法
  • 代码块
    • synchronized 声明的方法同一时间只能被一个线程访问。

2.5 transient

  • transient 变量
    • 序列化时被跳过

2.6 volatile

volatile 同步变量

  • 原子性:不保证
  • 可见性:保证,读写立刻与主内存交换
  • 有序性:保证,禁止指令重排序优化

1 类

类是构造对象的模板。

由类构造对象的过程称为创建类的实例。

一个源文件中,只能有一个公共类,类名必须与文件名相同。

1.1 类之间的关系

最常见的关系有:

  1. 依赖(uses-a):一个类的方法需要操纵另一个类的对象
  2. 聚合(has-a):类 A 的对象包含着类 B 的对象
  3. 继承(is-a):继承是一种用于表示特殊与一般的关系,父类更一般

应该尽可能地将相互依赖的类减至最少。

1.2 构造器

  1. 构造器和类名同名,参数不限,没有返回值。
  2. 构造器中的局部变量会覆盖同名实例域。
  3. 若未手动编写构造器,会默认提供一个无参构造器,设默认值。手动提供构造器后,不会自动提供无参构造器。
  4. 构造器不能被继承,因此不能被重写,但可以被重载。
  5. 父类与子类的构造函数调用次序:若子类构造器没有显式调用父类构造器,不管子类构造器有无参数,都默认调用父类无参构造器。

1.3 finalize 方法

可以为任何一个类添加 finalize 方法,将在垃圾回收器清除对象之前调用。不要依赖,不能保证被调用。

1.4 Object:所有类的超类

如果重新定义 equals 方法,就必须重新定义 hashCode 方法。eauals 与 hashCode 的定义必须一致:如果 x.eauals(y) 返回 true,那么 x.hashCode() 必须与 y.hashCode() 具有相同的值。

1)equals()方法
等价关系

两个对象具有等价关系,需要满足以下五个条件:

  • 自反性

    x.equals(x); // true
  • 对称性

    x.equals(y) == y.equals(x); // true
  • 传递性

    if (x.equals(y) && y.equals(z))
    x.equals(z); // true;
  • 一致性

    • 多次调用 equals() 方法结果不变
    x.equals(y) == x.equals(y); // true
  • 与 null 的比较

    • 对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false
    x.equals(null); // false;
等价与相等
  • 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
  • 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
实现
  • 检查是否为同一个对象的引用,如果是直接返回 true;
  • 检查是否是同一个类型,如果不是,直接返回 false;
  • 将 Object 对象进行转型;
  • 判断每个关键域是否相等。
2)hashCode()

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

在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象哈希值也相等。

HashSet 和 HashMap 等集合类使用了 hashCode() 方法来计算对象应该存储的位置,因此要将对象添加到这些集合类中,需要让对应的类实现 hashCode() 方法。

3)toString()

默认返回 ClassName@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。

4)clone()

clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。

clone() 方法并不是 Cloneable 接口的方法,Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。

使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。

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

1.5 枚举类

枚举类定义的是一个类,有着指定的几个实例。
比较两个枚举类型的值时,不需要调用 equals 方法,而直接使用”==”就可以了。

2 对象

2.1 创建对象

  1. 用 new 语句创建对象
  2. 运用反射
  3. 调用对象的 clone() 方法
  4. 运用反序列化手段,调用 java.io.ObjectInputStream 对象的 readObject() 方法

(1) 和 (2) 都会明确的显式的调用构造函数;(3) 是在内存上对已有对象的影印,所以不会调用构造函数;(4) 是从文件中还原类的对象,也不会调用构造函数。

2.2 对象克隆

有两种方式:

  • 实现 Cloneable 接口并重写 Object 类中的 clone() 方法;
  • 实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。

2.3 序列化

  • 对象序列化(Serializable)是指将对象转换为字节序列的过程,而反序列化则是根据字节序列恢复对象的过程。只有实现了 Serializable 和 Externalizable 接口的类的对象才能被序列化。
  • java.io.ObjectOutputStream 代表对象输出流,它的 writeObject(Objectobj) 方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
  • java.io.ObjectInputStream 代表对象输入流,它的 readObject() 方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

3 方法

3.1 方法签名

方法签名只有方法名参数,没有返回值。也就是说,不能有两个名字相同、参数也相同而返回值不同的方法。

3.2 隐式参数与显式参数

隐式参数是出现在方法名前的类对象(this),显式参数位于方法名后面的括号中。

使用 this 可以区分开隐式参数的类对象的实例域和局部变量

3.3 方法参数

Java 方法参数是值传递,不是引用传递。

方法在执行时,先定义了局部变量,这些局部变量指向,传入参数的指向。对局部变量重新指向时,完全不影响原本传入参数的那些指向。

  • 值传递:方法接收的是调用者提供的值。
  • 引用传递:方法接受的是调用所对应的变量地址。

3.4 参数变量可变的方法

Object… 参数类型与 Object[] 完全一样,省略号表明这个方法可以接收任意数量的的对象。


面向对象

1 抽象

抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。

2 封装

把数据和行为隐藏,只能通过暴露的接口访问数据。

3 继承

通过扩展一个类来建立另一个新的类。

3.1 慎用继承

  1. 将公共操作和域放在超类。
  2. 不要使用受保护的域。
  3. 使用继承实现”is-a”。
  4. 除非所有继承的方法都有意义,否则不要使用继承。
  5. 在覆盖方法时,不要改变预期的行为。
  6. 使用多态,而非类型信息。
  7. 不要过多地使用反射。

3.2 强制类型转换

只能在继承层次内进行类型转换。

在将超类转换成子类之前,应该使用 instanceof 进行检查。

  • 上溯造型(Upcasting):把衍生类型当作它的基本类型处理
  • 下溯造型(Downcasting):向下转型,超类可用子类置换。

3.3 重写(override)

存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。

为了满足里式替换原则,重写有以下三个限制:

  • 子类方法的访问权限必须大于等于父类方法;
  • 子类方法的返回类型必须是父类方法返回类型或为其子类型。
  • 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。

使用 @Override 注解,可以让编译器帮忙检查是否满足上面的三个限制条件。

在调用一个方法时,先从本类中查找看是否有对应的方法,如果没有再到父类中查看,看是否从父类继承来。否则就要对参数进行转型,转成父类之后看是否有对应的方法。总的来说,方法调用的优先级为:

  • this.func(this)
  • super.func(this)
  • this.func(super)
  • super.func(super)

3.4 重载(overload)

存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。

应该注意的是,返回值不同,其它都相同不算是重载。

4 多态

一个对象变量可以指示多种实际类型的现象被称为多态(polymorphism)。

Java 实现多态有三个必要条件:继承、重写、上溯造型。

虚拟机预先为每个类创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法。

4.1 动态绑定

在运行时能够自动选择调用哪个方法的现象称为动态绑定。

  1. 编译器会列举类方法表中同名方法,和超类方法表中同名且为 public 的方法。
  2. 编译器重载匹配参数类型,若有完全匹配的就选择,没有就匹配允许类型转换的方法,如果这样还是不能找到匹配项,编译器就会报错;同时如果找到多于一个匹配项,编译器也会报错。

4.2 静态绑定

如果是 private,static,final 方法或者构造器,那么编译器可以明确地知道该调用哪个方法,这种调用方式成为静态绑定。


运算符

1 数学函数与常量

  • Math.sqrt(x):平方根
  • Math.pow(x, y):幂运算,x 的 y 次幂
  • Math.floorMod(x, y):求余

2 自增与自减运算符

建议不要在表达式中使用 ++,容易带来疑惑和 bug

3 关系和 boolean 运算符

  • ==
  • !=
  • <
  • >
  • <=
  • >=
  • &&(短路)
  • ||(短路)
  • ?:

4 位运算符

  • 逻辑

    • 整型
      • | (或)
      • & (且)
      • ~ (非)
      • ^ (抑或)
    • 布尔值
      • 可用 &|,只是不短路
  • 位移

    • <<
      

      (左移)

      - `<<` 时 int 模 32,long 模 64,如 1 << 35 等于 1 << 3

      - ```
      >>
      (带符号右移) - “符号扩展”:若值为正,则在高位插入 0;若值为负,则在高位插入 1。
    • >>>
      

      (无符号右移)

      • “零扩展”:无论正负,都在高位插入0。
    • 不存在 <<<

5 括号与运算符级别

6 枚举类型


数组

1. 一维数组

1.1 声明

一旦创建了数组就不能改变它的大小。

int[] a = new int[n];
int a[] = new int[n];

1.2 初始化

数字数组初始化为 0。boolean 数组初始化 false。对象数组初始化 null。

int[] a = {1,2,3} ;
new int[]{1,2,3};

2. 多维数组

java 实际上没有多维数组,只有一维数组,多维数组被解释成“数组的数组”

2.1 声明

int[][] a = new int[n][]

2.2 初始化

int[][] A = {
{},
{}
...
}

3. Arrays 类

3.1 填充 fill()

Arrays.fill(type[] a,type v);

3.2 排序 sort()

优化的快排

//升序排序 
Arrays.sort(type[] a);
//给开始位到结束位排序
Arrays.sort(type[] a,int start,int end);

3.3 比较 equals()

比较元素是否相等

Arrays.equals(type[] a,type[] b);

3.4 查找 binarySearch()

二分查找,数组一定要是先排好序,返回最后一个找到的位置

Arrays.binarySearch(type[] a,type v);
//范围查找
Arrays.binarySearch(type[] a,type v,int start,int end);

3.5 拷贝 copyOf()

使用 Arrays 类的 copyOf 方法

//深度拷贝
int[] a = Arrays.copyOf(b,b.length);
//扩展
int[] a = Arrays.copyOf(b,b.length*2);
//部分拷贝
Arrays.copyOfRange(a,0,5)

3.6 转字符串 toString()

Arrays.toString(a)
//打印多维数组
Arrays.deepToString()

3.7 转列表 asList()

List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");

字符串

1 String 底层实现

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

底层是 char 或 byte 类型的 value 数组,value 数组也被声明为 final,这意味着数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。

编辑器可让字符串共享在常量池。

1.1 Java 8 - char 数组

在 Java 8 中,String 内部使用 char 数组存储数据。

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}

1.2 Java 9 - byte 数组

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

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final byte[] value;

/** The identifier of the encoding used to encode the bytes in {@code value}. */
private final byte coder;
}

1.3 不可变的好处

  • 可以缓存 hash 值
    • 因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
  • String Pool 的需要
    • 如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
  • 安全性
    • String 经常作为参数,String 不可变性可以保证参数不可变。
    • 例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。
  • 线程安全
    • String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

2 StringBuilder 和 StringBuffer

2.1 可变性与线程安全

  • String 不可变,因此是线程安全的
  • StringBuilder 可变,不是线程安全的,效率比 StringBuffer 高
  • StringBuffer 可变,是线程安全的,内部使用 synchronized 进行同步

2.2 底层实现

StringBuilder 和 StringBuffer 继承了 AbstractStringBuilder,AbstractStringBuilder 的 char 数组没有 final 关键字修饰,字符数组长度可变,所有 StringBuilder 和 StringBuffer 也是可变的

AbstractStringBuilder

AbstractStringBuilder(int capacity) {
value = new char[capacity];
}

StringBuilder

public StringBuilder() {
// StringBuilder 类继承 AbstractStringBuilder 抽象类
// 创建长度 16 的字符数组
super(16);
}
// 字符串拼接
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}

3 String Pool 与引用

字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。

在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。

  • new String():会在堆新建对象
  • intern() 方法: String 对象的 intern 方法会得到字符串对象在常量池中对应的版本的引用,如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用;
  • “”字面量:使用字面量的形式创建字符串,会自动地将字符串放入 String Pool 中
String s1 = "Programming";//先去常量池取,没有就新建对象放在常量池
String s2 = new String("Programming");//两个字符串对象,一个是常量池的"Programming",一个是用 new 创建在堆上的对象
String s3 = "Program";//常量池
String s4 = "ming";//常量池
String s5 = "Program" + "ming";//常量池
String s6 = s3 + s4;//堆
System.out.println(s1 == s2);//false
System.out.println(s1 == s5);//true
System.out.println(s1 == s6);//false
System.out.println(s1 == s6.intern());//true
System.out.println(s2 == s2.intern());//false

4 String 常用方法

4.1 初始化

1)使用字符串常量直接初始化
String s = "hello!";
2)使用构造方法创建并初始化
String s = new String(Object);

初始化源码

private final char value[];
// 本质是字符数组常量,所以不可变
public String() {
this.value = "".value;
}

4.2 操作

1)截取字符串
  • 单点截取:subString(开始下标)
  • 双点截取:subString(开始下标,结束下标)
2)拼接字符串
  • + 号
  • join:用一个定界符分割,String.join(“定界符”,“待合并字符串”…)

字符串的 + 操作其本质是创建了 StringBuilder 对象进行 append 操作,然后将拼接后的 StringBuilder 对象用 toString 方法处理成 String 对象

一般情况进行字符串拼接用 + 就可以,但是如果是循环拼接,则需要用 StringBuilder 的 append 来实现。

若不使用 StringBuilder 的 append 方法而使用 + 来进行连接。那么每次在循环体内都将会在 Heap 中创造一个新的 String 对象,造成资源浪费。

3)获取信息
  • 下标:indexOf(子字符),lastIndexOf(子字符)
  • 字符:charAt(下标)
  • 字节数组:getBytes()
  • 字符数组:toCharArray()
  • 长度:length()
4)替换字符串
  • 去掉前后空格:trim()
  • 子字符串:split(字符串),StringTokenizer()
5)判断字符串
  • 是否相等:.equals()
  • 不区分大小写:.equalsIgnoreCase()
  • 是否为空串:.length()==0 或 .equals(“”)
  • 是否为null:== null
  • 前缀:startsWith(前缀)
  • 后缀:endsWith(后缀)
  • 大小:compareTo()
6)字符串转换
  • 大/小写:
    • 大写:toLowerCase()
    • 小写:toUpperCase()
  • 字符串转换为基本类型
    • Long.parseLong(“1231”);
    • Double.parseDouble(“0.213”);
  • 基本类型转换为字符串
    • 基本数据类型变量 + “”
    • String.valueOf(其他类型的参数);

数据类型

数据类型分为基本类型和对象类型。

1. 基本类型

Java 中的基本类型有 8 种,其中有 4 种整型、2 种浮点型、char 和 boolean。还有 void 和 reference 类型

  1. 基本类型值存储在内存栈上。
  2. 基本类型所占空间大小固定。
  3. 所有数值类型都有正负号。

1.1 整型

类型 包装类 存储需求 取值范围 零值 备注
long Long 8 字节 64 bits -2^63 ~ +2^63-1 0L 后缀加L或l
int Integer 4 字节 32 bits -2^31 ~ +2^31-1 0 最常用,范围超20亿
short Short 2 字节 16 bits -2^15 ~ +2^15-1 (short)0 控制占用存储空间的大数组
byte Byte 1 字节 8 bits -2^7 ~ +2^7-1 (byte)0 底层文件处理
  1. 十六进制前缀 0x 或0X
  2. 八进制前缀 0
  3. 二进制前缀 0b 或 0B(Java 7 开始)
  4. 数字加下划线更易读,如 1_000_000,编译器会去掉(Java 7 开始)
  5. 没有后缀标识的整数默认为 int 类型

1.2 浮点

类型 包装类 存储需求 取值范围 零值 备注
double Double 8 字节 64 bits 大约-2^1024 ~ -2^-1074 —— 0 —— 2^-1074 ~ 2^1024(有效 15 位) 0.0d 后缀 D 或 d,最常用。
float Float 4 字节 32 bits 大约-2^128 ~ -2^-149 —— 0 —— 2^-149 ~ 2^128(有效 6~7 位) 0.0f 后缀 F 或 f,单精度库或大量数据
  1. 没有后缀F的浮点数默认是 double
  2. 浮点数不能绝对精确,因为二进制种无法精确表示 1/10,不允许误差应使用 BigDecimal 类。

浮点数的结构与整型不同,指数的底数为 2,尾数表示小数部分

类型 符号位(S) 指数位(E) 尾数位(M)
float 1 bit 8 bits 23 bits
double 1 bit 11 bits 52 bits

1.3 字符

类型 包装类 存储需求 取值范围 零值 备注
char Character 2 字节 16 bits int0-2^16-1 ,hex’\u0000’~’’\Uffff’ ‘\u0000’ char 字面量要用单引号括起来。
  • 转移序列 \u 可以出现在加引号的字符常量或字符串外(其它所有转义序列不可以)
  • 警告:Unicode 转移序列会在解析代码前得到处理
  • 警告:当心注释里的 \u,如“\u00A0”换行符,或“c:\user”,语法错误,因为没跟 4 个十六进制数
  • 解决方案:字符串中用转义符“\”来去除“\u”造成的不必要影响

1.4 布尔

boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。

JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。

JVM 支持 boolean 数组,但是是通过读写 byte 数组来实现的。

整型和布尔之间不能转换,不能用 0 当作 false,可以使用条件表达式

1.5 类型转换

不要强制转换 boolean 类型,可用条件表达式

自动类型转换图示:byte -> short(char) -> int -> long -> float -> double ,强制类型与之相反

1)自动类型转换

运算或方法调用时,系统会将较小的数据类型自动转换为较大的数据类型

  • 当使用 +、-、*、/、% 运算符对基本类型进行运算时,转换为较大的数据类型,如最大的小于 int,则转为 int
2)强制类型转换

强制类型将较大的数据类型转化为较小的数据类型,可能导致精度损失。

  • 当使用 +=、-=、*=、/=、%= 运算符对基本类型进行运算时,运算符右边的数值将首先被强制转换成与运算符左边数值相同的类型,然后再执行运算,且运算结果与运算符左边数值类型相同。

2. 包装类型

  • 所有的基本类型都有一个与之对应的类。包装类都是声明为 final的,不可变,不可被继承。
  • 对象引用存储在内存栈上,而对象本身的值存储在内存堆上。
  • 对一个对象进行操作时,我们真正操作的是它的句柄。
  • 自动装箱:基本类型自动转换成包装类,调用了 Integer.valueOf()
  • 自动拆箱:包装类自动转换为基本类型,调用了 X.intValue()

2.1 大数值

  • BigInteger 支持任意精度的整数。
  • BigDecimal 支持任意精度的浮点数字。
  • 转换方法: valueOf
  • 运算方法: add, multiply, divide 等

3. 缓存池

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

  • boolean:true, false
  • byte、short、int、long:-128 ~ 127
  • char:\u0000 ~ \u007F

在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。

3.1 IntegerCache

Integer 缓冲池 IntegerCache, 是静态类,上界可以通过参数调整。

加载的时候会初始化一个 int 数组,填上 -128 ~ 127 的值。

3.2 valueOf() 方法

valueOf() 方法会使用缓存池中的对象,多次调用会取得同一个对象的引用。

编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。


类加载器

一、类加载器

1 类加载过程

虚拟机只加载程序执行时所需要的类文件。假设程序从 MyProgram.class 开始运行,下面是虚拟机执行的步骤:

  1. 虚拟机有一个用于加载类文件的机制。
  2. 如果类拥有类型为另一个类的域,或者是拥有超类,那么这些类文件也会被加载。
  3. 接着,虚拟机执行类中的 main 方法。
  4. 如果 main 方法或者 main 调用的方法要用到更多的类,那么接下来就会加载这些类。

每个Java程序至少拥有三个类加载器

  • 启动类加载器(Bootstrap ClassLoader)

    ​ 这个类加载器负责负责加载JDK中的核心类库,这个类加载器完全由JVM控制

  • 扩展类加载器(Extendsion ClassLoader)

    ​ 这个类加载器负责加载\lib\ext目录下的类库

  • 应用程序类加载器(Application ClassLoader)

    ​ 这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的java类都是由这个类加载器加载

2 类加载器的层次结构

类加载器有一种父/子关系。除了引导类加载器外,每个类加载器都有一个父类加载器。父类加载失败子类才加载。

如果插件被打包为 JAR 文件,那就可以直接用 URLClassLoader 类的实例去加载这些类。

URL url = new URL("path");
URLClassLoader pluginLoader = new URLClassLoader(new URL[] {url});
Class<?> cl = pluginLoader.loadClass("mypackage.MyClass");

可以通过下面将其设置成为任何类加载器

Thread t = Thread.currentThread();
t.setContextClassLoader(loader);

助手方法可以获取这个上下文类加载器:

Thread t = Thread.currentThread();
ClassLoader loader = t.getCOntextClassLoader();
Class cl = loader.loaderClass(className);

当调用由不同的类加载器加载的插件类的方法时,进行上下文类加载器的设置是一种好的思路;或者,让助手方法的调用者设置上下文类加载器。

3 将类加载器作为命名空间

同一虚拟机中,可以有两个类的类名和包名都相同。类是由它的全名和类加载器来确定的。

4 编写自己的类加载器

编写自己的类加载器,只需要继承 ClassLoader 类,然后覆盖下面这个方法

findClass(String className)

ClassLoader 超类的 loadClass 方法用于将类的加载操作委托给其父类加载器去进行,只有当该类尚未加载并且父类加载器也无法加载该类时,才调用 findClass 方法。

如果要实现该方法,必须做到以下几点:

  1. 为来自本地文件系统或者其他来源的类加载其字节码
  2. 调用 ClassLoader 超类的 defineClass 方法,想虚拟机提供字节码。

5 字节码校验

当类加载器将新在加载的 Java 平台类的字节码传递给虚拟机,这些字节码首先要接受校验器的校验。校验器负责检查那些指令无法执行的明细那有破坏性的操作。出了系统类外,所有的类都要被校验。

下面是校验器执行的一些检查:

  • 变量要在使用之前进行初始化。
  • 方法调用与对象引用类型之间要匹配。
  • 访问私有类型和方法的规则没有被违反。
  • 对本地变量的访问都落在运行时堆栈内。
  • 运行时堆栈没有用溢出。

二、双亲委派原则

1 双亲委派原则

一个类加载器受到类加载的请求,它会把这个请求转交到它的父加载器去请求,如果上级还有加载器,就继续把请求上传,直到启动类加载器。然后找到就返回给子加载器,直到第一个发出请求的类加载器。如果最后还是没有找到,就让子加载器自己去找

img

2 破坏双亲委派原则

单一责任原则(SRP)不是绝对的。 它的存在有助于代码的可维护性和可读性。 但是您可能会不时看到解决方案,破坏SRP的模式,而且还可以。 其他原则也是如此,但是这次我想谈谈SRP。

在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。

2.1 线程上下文类加载器(contextClassLoader)

通过java.lang.Thread类中的getContextClassLoader()setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源,如下图所示,以jdbc.jar加载为例

img


分布式架构网络通信

一、RPC(远程过程调用)

1. RPC架构

  • 客户端(Client),服务的调用方。
  • 客户端存根(Client Stub),存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。
  • 服务端(Server),真正的服务提供者。
  • 服务端存根(Server Stub),接收客户端发送过来的消息,将消息解包,并调用本地的方法2

2.RPC调用过程

调用过程

RPC的目标是要把2、3、4、7、8、9这些步骤都封装起来。

注意:无论是何种类型的数据,最终都需要转换成二进制流在网络上进行传输,数据的发送方需要将对象转换为二进制流,而数据的接收方则需要把二进制流再恢复为对象。

在java中RPC框架比较多,常见的有Hessian、gRPC、Thrift、Dubbo 等,其实对于RPC框架而言,核心模块就是通讯和序列化

ps:RPC并不是一个具体的技术,而是指整个网络远程调用过程。

二、RMI(远程方法调用)

Java RMI 指的是远程方法调用 (Remote Method Invocation),是java原生支持的远程调用

1.RMI的架构

客户端:

  • 存根/桩(Stub):远程对象在客户端上的代理;

  • 远程引用层(Remote Reference Layer):解析并执行远程引用协议;

  • 传输层(Transport):发送调用、传递远程方法参数、接收远程方法执行结果。

服务端:

  • 骨架(Skeleton):读取客户端传递的方法参数,调用服务器方的实际对象方法, 并接收方法执行后的返回值;
  • 远程引用层(Remote Reference Layer):处理远程引用后向骨架发送远程方法调用;
  • 传输层(Transport):监听客户端的入站连接,接收并转发调用到远程引用层。

注册表(Registry):

  • 以URL形式注册远程对象,并向客户端回复对远程对象的引用。

rmi

RMI和RPC的区别

1:方法调用方式不同:

  • RMI中是通过在客户端的Stub对象作为远程接口进行远程方法的调用。每个远程方法都具有方法签名。如果一个方法在服务器上执行,但是没有相匹配的签名被添加到这个远程接口(stub)上,那么这个新方法就不能被RMI客户方所调用。
  • RPC中是通过网络服务协议向远程主机发送请求,请求包含了一个参数集和一个文本值,通常形成“classname.methodname(参数集)”的形式。RPC远程主机就去搜索与之相匹配的类和方法,找到后就执行方法并把结果编码,通过网络协议发回。

2:适用语言范围不同:

  • RMI只用于Java;
  • RPC是网络服务协议,与操作系统和语言无关。

3:调用结果的返回形式不同:

  • Java是面向对象的,所以RMI的调用结果可以是对象类型或者基本数据类型;
  • RMI的结果统一由外部数据表示,这种语言抽象了字节序类和数据类型结构之间的差异。