Java-初级&高级特性梳理

OOP

OOP的三大特性

面向对象的程序设计方法具有三个基本特征:封装、继承、多态。

封装

封装:将对象的实现细节隐藏起来,然后通过一些公用方法来暴露该对象的功能。

继承

继承是面向对象实现软件复用的重要手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法;

多态

多态分为编译时多态和运行时多态。

编译时多态表现为重载,重载是同一类下的不同方法,通过同一函数名不同的参数列表表现出对象的多样性。

运行时多态表现为继承、重写、向上转型。子类继承父类之后,重写父类方法后通过向上转型,调用重写方法时表现出不同的子类的行为特征。

Q:为什么用接口定义变量,而非具体的实现类?(比如List<Integer> list = new ArrayList<>();List为接口)

A:用接口定义变量只需关注接口特性,而无需关注具体实现,这体现面向接口编程思想;同时,如果想换一种实现类,只需更改这一行代码就可以实现。

向上转型 & 向下转型

例子参考

向上转型

用子类实例化父类对象,属于自动转换。

向上转型的意义在于不同子类可以重写父类方法,可以统一用定义的父类对象调用得到子类方法的结果,减少了代码量,体现了抽象编程的思想。

子类重写父类方法的条件:1、父类方法对子类可见(不能是private);2、子类和父类的方法必须是实例方法(不能是static);3、父类和子类的方法需要有相同的函数名、参数列表、返回类型(子类可以是父类的子类型);4、子类访问权限不能小于父类;5、子类不能抛出比父类范围更大的异常;参考地址

向下转型

用父类实例化子类对象,强制转换。用 instance of 进行判断后强转。

重载 & 重写

重载

重载是同一个类中的不同方法,方法名相同而参数列表不同,与返回类型和作用域无关。

重写

重写发生在子类父类中,子类对父类的方法重写时,函数名和参数列表要相同,返回类型要小于等于父类,作用域要大于等于父类,抛出异常的范围要小于等于父类。

Q:构造方法能重写吗?

构造方法不能重写。

首先,子类重写父类方法时,方法名要保持一致(子类的构造函数名要和父类的构造函数名一致);其次,构造方法要和类名保持一致(子类构造方法要和子类名保持一致,父类构造方法要和父类名一致);这就推出子类名要和父类名一致,但是二者是不同的,所以构造方法不能重写。

Object类的方法

  1. clone() : 浅拷贝,需要实现Cloneable接口。
  2. toString()
  3. hashCode() : 获取对象的哈希值,一般用于比较两个对象是否相等的初判断条件
  4. equals() : 判断两个对象是否相等,一般情况下equals()==是不一样的,只有在Object中两者才是一样的
  5. getClass() : 获取运行时类型
  6. wait() : 是当前线程等待该对象的锁
  7. notify() : 唤醒在该对象等待的某线程
  8. notifyAll() : 唤醒在该对象等待的所有线程

深拷贝和浅拷贝

实例参考深拷贝实现参考

浅拷贝:只是对被拷贝对象的变量进行拷贝,如果被拷贝对象的变量指向上一层对象,那拷贝对象的变量也只想那个上层对象,此时修改上层对象,拷贝对象的变量和被拷贝的对象的变量同时修改。

深拷贝:对被拷贝的变量所指向的上层对象也进行拷贝,此时如果修改被拷贝对象上层对象属性,拷贝后的对象不会受到影响。

深拷贝的实现:

  1. 通过重写Cloneable的clone方法,对拷贝对象引用的所有对象也进行拷贝
  2. 通过序列化的方式,写到输出流,再读取出来,自然得到的是一个新对象

关键字

super

super关键字是一个引用变量,用于引用直接父类对象。

  1. super.fatherAttribute :访问父类属性
  2. super.fatherMethod() :访问父类方法
  3. super() :访问父类构造方法

static

static是为了满足两个需求产生的:1、为特定域分配单一存储空间;2、不需要通过反复创建对象调用方法。

java类有五个组成部分:属性、成员方法、构造方法、初始化块、内部类。static可以作用在除构造方法的其他成分上,被static修饰的成分是属于类的,而非某个实例。

对static使用的说明:

  1. static类可以被继承,但被static修饰的方法不能在子类被重写(方法重写是运行绑定的,而static方法是编译时绑定的);
  2. static方法不能引用非static资源,但非static方法可以引用static资源;
  3. static修饰内部类也可以被外部类的实例访问,因为static作用的成分属于公共资源;

静态内部类

final

final修饰在不同成分上表现出不同的特征。

  1. 修饰类:不可以被继承;
  2. 修饰方法:不可以被重写;
  3. 修饰变量:获得初始值,就不可以被修改。

被final修饰的常见的类:八大封装类,void,字符串类(String, Stringbuffer, StringBuilder),Array等。

抽象类 & 接口

抽象类

Q:java为什么要有抽象类?

提供实现不同子类公用的方法。

接口

抽象类和接口的区别

泛型

泛型定义

泛型是jdk5引入的新特性,本质为了参数化类型,提供了类型安全检测机制,实现了一次编写模板,多次复用。

List里可以装不同类型的元素,在编写底层List底层代码时,如果不使用泛型,对于每个方法都要根据不同如参类型进行重载,代码重用性极差;
同时,底层实现是Object[],传不同的数据类型,要进行强制转换,这会导致容易产生类型转换错误等。

1
2
3
4
5
6
7
public class ArrayList<T> {
private T[] array;
private int size;
public void add(T e) {...}
public void remove(int index) {...}
public T get(int index) {...}
}

❗️❗️泛型不是一种基本类型,不能通过getClass()方法获取Class类型。

编写泛型

编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用T, E or K,V等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。在函数返回类型,和入参类型需要使用泛型的地方都需要用占位符占位。

1
2
3
4
5
public V put(K key, V value) {

return cache.put(key, value);

}

泛型的Java实现(擦拭法)

泛型只在编译阶段有效,在编译过程中会进行类型擦除,并在对象进入和离开方法的时候进行类型转换和类型检测,而泛型信息不会进入到运行时阶段。

Java语言的泛型实现方式是擦拭法(Type Erasure)

擦拭法是指,JVM完全屏蔽泛型,所有的类型转换工作都是编译器做的。

1
2
3
4
5
6
7
8
9
// coding
Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();

// jvm
Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();

擦拭法决定了泛型

  1. 不能是基本类型,例如:int;
  2. 不能获取带泛型类型的Class,例如:Pair.class;
  3. 不能判断带泛型类型的类型,例如:x instanceof Pair
  4. 不能实例化T类型,例如:new T()。

extends & super通配符

向上转型

❗️❗️向上转型是谁转?是外层的类型转,而不是<>里的类型转!

对于ArrayList<String>来说,向上转型的结果就是List<String>

但是对于ArrayList<Integer>ArrayList<Number>并不是其向上转型的结果!

如果想对<>里面的内容进行向上或向下类型转换,要怎么做? 向上转->extends,向下转->super

extends 上界通配符

通配符的一个重要限制:方法参数签名setFirst(? extends Number)无法传递任何Number的子类型给setFirst(? extends Number) 使用通配符表示: 1. 使用extends通配符表示可以读,不能写; 2. 使用类似< T extends Number>定义泛型类时表示:泛型类型限定为Number以及Number的子类。
1
2
3
4
5
6
7
8
9
// extends的作用是限制set,只get
int sumOfList(List<? extends Integer> list) {
int sum = 0;
for (int i=0; i<list.size(); i++) {
Integer n = list.get(i);
sum = sum + n;
}
return sum;
}
### super 下界通配符 使用通配符表示: 1. 允许调用set(? super Integer)方法传入Integer的引用; 2. 不允许调用get()方法获得Integer的引用。
1
2
3
4
5
6
7
8
9
public class Collections {
// 把src的每个元素复制到dest中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i);
dest.add(t);
}
}
}
`Collections.copy`的作用是把一个List的每个元素依次添加到另一个List中。它的第一个参数是`List`,表示目标List,第二个参数`List`,表示要复制的List。我们可以简单地用for循环实现复制。在for循环中,我们可以看到,对于类型``的变量src,我们可以安全地获取类型T的引用,而对于类型``的变量dest,我们可以安全地传入T的引用。 这个`copy()`方法的定义就完美地展示了extends和super的意图: `copy()`方法内部不会读取dest,因为不能调用`dest.get()`来获取T的引用; `copy()`方法内部也不会修改src,因为不能调用`src.add(T)`。 ### ? 无边界通配符

通配符既没有extends,也没有super,因此:

  1. 不允许调用set(T)方法并传入引用(null除外);
  2. 不允许调用T get()方法并获取T引用(只能获取Object引用)。

无限定通配符<?>很少使用,可以用替换,同时它是所有类型的超类。

泛型的使用

Q:在哪些地方可以使用泛型?泛型有哪些使用方式?

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。

泛型类

泛型类,就是用泛型来定义类。通过泛型可以完成对一组类的操作对外开放相同的接口。

最典型的就是各种容器类,如:List、Set、Map。

1
2
3
4
5
class GenericClass<T> {
public void method(T t) {
System.out.println(t);
}
}

泛型接口

泛型接口,就是用泛型来定义接口。泛型接口常被用在各种类的生成器中。

1
2
3
4
//定义一个泛型接口
public interface Generator<T> {
public T next();
}

泛型方法

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** 
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
* 如:public <T,K> K showKeyName(Generic<T> container){
* ...
* }
*/
public <T> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
//当然这个例子举的不太合适,只是为了说明泛型方法的特性。
T test = container.getKey();
return test;
}

泛型类中的泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class GenerateTest<T>{
public void show_1(T t){
System.out.println(t.toString());
}

//在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
//由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
public <E> void show_3(E t){
System.out.println(t.toString());
}

//在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
public <T> void show_2(T t){
System.out.println(t.toString());
}
}

可变参数的泛型方法

1
2
3
4
5
6
public <T> void printMsg( T... args){
for(T t : args){
Log.d("泛型测试","t is " + t);
}
}
printMsg("111",222,"aaaa","2323.4",55.55);

静态泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public class StaticGenerator<T> {
....
....
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show(T t){..},此时编译器会提示错误信息:
"StaticGenerator cannot be refrenced from static context"
*/
public static <T> void show(T t){

}
}

泛型数组

不能创建一个确切的泛型类型的数组!原因在于JVM泛型擦除在运行时装入一个元素不会异常,但是在取值的时候可能会出现不一致的情况。

1
2
3
List<String>[] ls = new ArrayList<String>[10];   // not allowed !
List<?>[] ls = new ArrayList<?>[10]; // allowed !
List<String>[] ls = new ArrayList[10]; // alllowed !

其他问题

  1. Array中可以用泛型吗?
    Array是不可以使用泛型的。这是因为List可以提供编译期的类型安全保证,而Array却不能。

参考资料

  1. https://blog.csdn.net/s10461/article/details/53941091

注解

反射

核心知识点

反射是Java在运行时,可以动态地获取对象的所有属性和方法的一种机制。

反射通过Class类获取类的class对象;通过Constructor类反映类的构造方法;通过Field类反映类的属性;通过Method类反映类的方法。

Class类是一个类(java.lang),其实例表示java运行时的类或接口。

反射的使用

Class类对象的获取

在类加载的时候,jvm会创建一个class对象。

获取class对象的方式的主要有三种:

  1. 根据类名:类名.class
  2. 根据对象:对象.getClass()
  3. 根据全限定类名:Class.forName(全限定类名)
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
37
38
39
40
41
@Test
public void classTest() throws Exception {
// 获取Class对象的三种方式
logger.info("根据类名: \t" + User.class);
logger.info("根据对象: \t" + new User().getClass());
logger.info("根据全限定类名:\t" + Class.forName("com.test.User"));
// 常用的方法
logger.info("获取全限定类名:\t" + userClass.getName());
logger.info("获取类名:\t" + userClass.getSimpleName());
logger.info("实例化:\t" + userClass.newInstance());
}

// ...
package com.test;

public class User {
private String name = "init";
private int age;
public User() {}
public User(String name, int age) {
super();
this.name = name;
this.age = age;
}
private String getName() {
return name;
}
private void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User [name=" + name + ", age=" + age + "]";
}
}

Constructor类

获取Constructor对象是通过Class类中的方法获取的。

方法返回值 方法名称 方法说明
static Class<?> forName(String className) 返回与带有给定字符串名的类或接口相关联的 Class 对象。
Constructor getConstructor(Class<?>… parameterTypes) 返回指定参数类型、具有public访问权限的构造函数对象
Constructor<?>[] getConstructors() 返回所有具有public访问权限的构造函数的Constructor对象数组
Constructor getDeclaredConstructor(Class<?>… parameterTypes) 返回指定参数类型、所有声明的(包括private)构造函数对象
Constructor<?>[] getDeclaredConstructor() 返回所有声明的(包括private)构造函数对象
T newInstance() 调用无参构造器创建此 Class 对象所表示的类的一个新实例。

Constructor类常用的API:

方法返回值 方法名称 方法说明
Class getDeclaringClass() 返回 Class 对象,该对象表示声明由此 Constructor 对象表示的构造方法的类,其实就是返回真实类型(不包含参数)
Type[] getGenericParameterTypes() 按照声明顺序返回一组 Type 对象,返回的就是 Constructor对象构造函数的形参类型。
String getName() 以字符串形式返回此构造方法的名称。
Class<?>[] getParameterTypes() 按照声明顺序返回一组 Class 对象,即返回Constructor 对象所表示构造方法的形参类型
T newInstance(Object… initargs) 使用此 Constructor对象表示的构造函数来创建新实例
String toGenericString() 返回描述此 Constructor 的字符串,其中包括类型参数。

Field类

获取Field对象是通过Class类中的方法获取的。

方法返回值 方法名称 方法说明
Field getDeclaredField(String name) 获取指定name名称的(包含private修饰的)字段,不包括继承的字段
Field[] getDeclaredField() 获取Class对象所表示的类或接口的所有(包含private修饰的)字段,不包括继承的字段
Field getField(String name) 获取指定name名称、具有public修饰的字段,包含继承字段
Field[] getField() 获取修饰符为public的字段,包含继承字段

Field类常用的API:

方法返回值 方法名称 方法说明
void set(Object obj, Object value) 将指定对象变量上此 Field 对象表示的字段设置为指定的新值。
Object get(Object obj) 返回指定对象上此 Field 表示的字段的值
Class<?> getType() 返回一个 Class 对象,它标识了此Field 对象所表示字段的声明类型。
boolean isEnumConstant() 如果此字段表示枚举类型的元素则返回 true;否则返回 false
String toGenericString() 返回一个描述此 Field(包括其一般类型)的字符串
String getName() 返回此 Field 对象表示的字段的名称
Class<?> getDeclaringClass() 返回表示类或接口的 Class 对象,该类或接口声明由此 Field 对象表示的字段
void setAccessible(boolean flag) 将此对象的 accessible 标志设置为指示的布尔值,即设置其可访问性

Method类

获取Method对象是通过Class类中的方法获取的。

方法名称 方法说明
Method getDeclaredMethod(String name, Class<?>… parameterTypes) 返回一个指定参数的Method对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法。
Method[] getDeclaredMethod() 返回 Method 对象的一个数组,这些对象反映此 Class 对象表示的类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。
Method getMethod(String name, Class<?>… parameterTypes) 返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法。
Method[] getMethods() 返回一个包含某些 Method 对象的数组,这些对象反映此 Class 对象所表示的类或接口(包括那些由该类或接口声明的以及从超类和超接口继承的那些的类或接口)的公共 member 方法。

Method类常用的API:

方法返回值 方法名称 方法说明
Object invoke(Object obj, Object… args) 对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。
Class<?> getReturnType() 返回一个 Class 对象,该对象描述了此 Method 对象所表示的方法的正式返回类型,即方法的返回类型
Type getGenericReturnType() 返回表示由此 Method 对象所表示方法的正式返回类型的 Type 对象,也是方法的返回类型。
Class<?>[] getParameterTypes() 按照声明顺序返回 Class 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型。即返回方法的参数类型组成的数组
Type[] getGenericParameterTypes() 按照声明顺序返回 Type 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型的,也是返回方法的参数类型
String getName() 以 String 形式返回此 Method 对象表示的方法名称,即返回方法的名称
boolean isVarArgs() 判断方法是否带可变参数,如果将此方法声明为带有可变数量的参数,则返回 true;否则,返回 false。
String toGenericString() 返回描述此 Method 的字符串,包括类型参数。

异常

异常的层次结构

Java异常结构图

结构图解析

  • Throwable :异常和错误的超类,只有Throwable的实例才能被throw或者catch。
    • Error : 程序中无法处理的错误,表示程序运行时JVM出现问题。比如OOM、StackOverflow;
    • Exception : 程序本身可以捕获并且可以处理的异常。
      • RuntimeException : 由程序逻辑错误引起的,编译器不会检查,也不会捕获处理。(NullPointerExceptio,IndexOutOfBoundsException)
      • 非运行时异常 :必须进行处理的异常,如果不处理,程序就不能编译通过。(IOException, SQLException)
  • checkedExceptions :除了RuntimeException外的Exception类及其子类;这种是编译器可以检测的,需要try…catch…或者throws,否则会报错。
  • uncheckedExceptions : 编译器不要求强制处理的异常。

Q:什么时候要往上层抛异常?

一般如果程序中出现checkedException,编译器会提示进行异常处理。而抛异常一般指向上一层方法抛异常(因为本层如果可以处理就try…catch…捕获了)。而向上抛异常一般可能有以下两点考虑:

  • 传递一个具体的危险信号,让调用方知道;(catch后抛出一个新的类型的异常)
  • 本层方法没有能力处理,返回给调用方处理;(当前层方法被不只一个地方调用,业务逻辑并不一致,不能做同一处理,需要向上层抛出;)

Q:怎么抛异常?

在方法里可以throw一个异常,或者在方法签名throws若干异常。

IO

序列化

序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。

一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口。

把一个Java对象变为byte[]数组,需要使用ObjectOutputStream,从一个字节流读取Java对象用ObjectInputStream

IO模型

todo

IO多路复用

todo

容器

容器间的继承关系

Java集合框架体系

Java容器继承关系图

HashMap

put 过程分析

  1. 第一次put的时候,会触发resize操作,第一次resize默认的数组大小是16;
  2. 通过hash映射,得到当前key要put到的数组位置(数组下标是 arrlen & hash(key) 得到的);
  3. 如果当前数组里的位置为空,新建一个KV的node放到数组里;(node 包含的属性有key, value, next, hash)
  4. 如果数组里已经有数据,取出当前结点判断key与要put的key是否一致,如果一致说明要覆盖旧值,如果不一致,说明哈希冲突;
  5. 如果哈希冲突,需要对冲突的node进行类型判断,如果是红黑树,需要通过红黑树的插值方法完成put操作;否则是链表,通过拉链法完成插值;
  6. 拉链法插值的过程为:要遍历链表,如果找到node的key与当前put的node的key一致,进行覆盖;否则将node加到链表尾部;链表长度超过8要转成红黑树的操作;
  7. 哈希冲突判断完,接下来剩下两个判断操作;一个是在key相等时进行覆盖;另一个是如果因为put达到阈值要进行扩容。
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

// 第四个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作
// 第五个参数 evict 我们这里不关心
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度
// 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 找到具体的数组下标,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);

else {// 数组该位置有数据
Node<K,V> e; K k;
// 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果该节点是代表红黑树的节点,调用红黑树的插值方法,本文不展开说红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 到这里,说明数组该位置上是一个链表
for (int binCount = 0; ; ++binCount) {
// 插入到链表的最后面(Java7 是插入到链表的最前面)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 8 个
// 会触发下面的 treeifyBin,也就是将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在该链表中找到了"相等"的 key(== 或 equals)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node
break;
p = e;
}
}
// e!=null 说明存在旧值的key与要插入的key"相等"
// 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

扩容分析

  1. 在扩容代码里,主要有两个内容;1、针对capacity 和threshold 进行更新;2、进行数据迁移;
  2. 针对容量和阈值进行更新的时候,有两种情况;1、第一次put 数组初始化的hashmap;2、对有值的数组进行扩容;
  3. 对有值的数组进行扩容为一般情况,采取的扩容策略为将cap和thr分别扩大一倍(边界case:对>=2^30的cap直接扩容到Integer.MAX_VALUE);
  4. 如果是初始化的数组,就直接返回了;如果是已经有值的数组,需要进行数据迁移;
  5. 在数据迁移的时候,要将链表拆成两个链接。具体操作有两步;1、将数据迁移,用拉链法解决hash冲突;2、将两条链表分别挂在数组对应的下标上;
  6. 数据迁移时,遍历数组每一个元素:1、如果只有一个值,按key进行hash映射到新的地址就可以了;2、如果是红黑树,不展开;3、如果是链表,拿key的hash与oldcap做&,将最高位为0or1作为两个链表其中的一个的区分,在新得到的位置用拉链法解决哈希冲突(如果需要的话);
  7. 将链表挂载数组时,将低位链表挂在原位置上,将高位链表挂在原位置高一个oldcap的位置上。
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) { // 对应数组扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 将数组大小扩大一倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 将阈值扩大一倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候
newCap = oldThr;
else {// 对应使用 new HashMap() 初始化后,第一次 put 的时候
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}

if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;

// 用新的数组大小初始化新的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; // 如果是初始化数组,到这里就结束了,返回 newTab 即可

if (oldTab != null) {
// 开始遍历原数组,进行数据迁移。
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 如果该数组位置上只有单个元素,那就简单了,简单迁移这个元素就可以了
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果是红黑树,具体我们就不展开了
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 这块是处理链表的情况,
// 需要将此链表拆成两个链表,放到新的数组中,并且保留原来的先后顺序
// loHead、loTail 对应一条链表,hiHead、hiTail 对应另一条链表,代码还是比较简单的
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
// 第一条链表
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
// 第二条链表的新的位置是 j + oldCap,这个很好理解
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

get 过程分析

  1. 根据key做hash与当前数组容量做& 确定下标位置;
  2. 如果当前位置的元素的key和get的key一致,直接返回;
  3. 否则看当前元素类型,如果是红黑树,调用红黑树的查找方法查找;如果是链表,遍历拉链进行查找返回;
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
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 判断第一个节点是不是就是需要的
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 判断是否是红黑树
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);

// 链表遍历
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

TreeMap

todo

LinkedHashMap

todo

Java8 新特性

Lambda表达式

简介

函数式编程

Java8之前用的都是静态方法或实例方法,Java8新增了函数方法,即把函数作为基本运算单元,函数可以作为变量,可以接收函数,还可以返回函数。

lambda表达式是函数式编程,lambda表达式的类型必须是函数式接口,被@FunctionalInterface注解修饰。参考地址

函数式编程是将函数(一段操作)作为一个基本单位进行传递。以前的Java中参数只能是具体的变量,函数式编程打破这一规范,可以将整个方法作为一个参数传递。

lambda表达式语法

1
() -> {}();(参数类型 参数名称) -> { 代码语句 };//小括号里无参数则留空(),有一个参数括号可以省略,多个参数用逗号隔开

FunctionalInterface(函数式接口)

有且仅有一个抽象方法的接口。比如:ComparatorCallableRunnable 等。

虽然Comparator接口有很多方法,但只有一个抽象方法int compare(T o1, T o2),其他的方法都是default方法或static方法。另外注意到boolean equals(Object obj)是Object定义的方法,不算在接口方法内。因此,Comparator也是一个FunctionalInterface。

方法引用

方法引用:使用操作符 “ ::” 将方法名和对象或类的名字分隔开来。主要有:对象::实例方法 类::静态方法 类::构造方法

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
// 
public class Main {
public static void main(String[] args) {
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
Arrays.sort(array, Main::cmp); // 类::静态方法
System.out.println(String.join(", ", array));
}

static int cmp(String s1, String s2) {
return s1.compareTo(s2);
}
}

public class Main {
public static void main(String[] args) {
List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = names.stream().map(Person::new).collect(Collectors.toList()); // 类::构造方法
System.out.println(persons);
}
}

class Person {
String name;
public Person(String name) {
this.name = name;
}
public String toString() {
return "Person:" + this.name;
}
}

特性

  1. lambda表达式内可以使用方法引用,仅当该方法不修改lambda表达式提供的参数;
  2. lambda内部可以使用静态、非静态和局部变量,这称为lambda内的变量捕获;
  3. lambda表达式只能引用 final 或 final 局部变量,这就是说不能在lambda内部修改定义在域外的变量;
1
2
3
4
5
6
7
// 1.
list.forEach((String s) -> System.out.println("*" + s + "*"));
// 3.
List<Integer> primes = Arrays.asList(new Integer[]{2, 3,5,7});
int factor = 2;
primes.forEach(element -> { System.out.println(factor*element); }); // run
primes.forEach(element -> { factor++; }); // error

Steam

Steam 的特点

java.io java.util.stream
存储 顺序读写的bytechar 顺序输出的任意Java对象实例
用途 序列化至文件或网络 内存计算/业务逻辑
java.util.List java.util.stream
元素 已分配并存储在内存 可能未分配,实时计算
用途 操作一组已存在的Java对象 惰性计算

惰性计算:一个Stream转换为另一个Stream时,实际上只存储了转换规则,并没有任何计算发生。

1
2
3
4
5
int result = createNaturalStream() // 创建Stream
.filter(n -> n % 2 == 0) // 任意个转换
.map(n -> n * n) // 任意个转换
.limit(100) // 任意个转换
.sum(); // 最终计算结果

Steam 的创建

Stream.of()

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("A", "B", "C", "D");
// forEach()方法相当于内部循环调用,
// 可传入符合Consumer接口的void accept(T t)的方法引用:
stream.forEach(System.out::println);
}
}

基于数组或Collection

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
Stream<String> stream2 = List.of("X", "Y", "Z").stream();
stream1.forEach(System.out::println);
stream2.forEach(System.out::println);
}
}

基于Supplier

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public static void main(String[] args) {
Stream<Integer> natual = Stream.generate(new NatualSupplier());
// 注意:无限序列必须先变成有限序列再打印:
natual.limit(20).forEach(System.out::println);
}
}

class NatualSupplier implements Supplier<Integer> {
int n = 0;
public Integer get() {
n++;
return n;
}
}

map

Map就是把一个Stream转换为另一个Stream。

1
2
Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s2 = s.map(n -> n * n);

reduce

reduce是一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果,即一个 Java对象。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
// acc 是上一层计算的结果,0是acc的初始化值
int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (acc, n) -> acc + n);
System.out.println(sum); // 45
}
}

filter

所谓filter()操作,就是对一个Stream的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.filter(n -> n % 2 != 0)
.forEach(System.out::println); // 1 3 5 7 9
}
}

输出集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 输出为List
Stream<String> stream = Stream.of("Apple", "", null, "Pear", " ", "Orange");
List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
System.out.println(list);

// 输出为Map
Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
Map<String, String> map = stream
.collect(Collectors.toMap(
// 把元素s映射为key:
s -> s.substring(0, s.indexOf(':')),
// 把元素s映射为value:
s -> s.substring(s.indexOf(':') + 1)));
System.out.println(map);

// 输出为数组
List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);