jianping5

V1

2022/04/26阅读:24主题:萌绿

Java 基础

Java 基础

对象类型转换

一、向上转型

父类引用指向子类对象为向上转型

fatherClass obj = new sonClass();

向上转型就是把子类对象赋给父类引用,不用强制转换。可以调用父类类型中的所有成员,不能调用子类类型中的特有成员,最终运行效果看子类的具体实现(方法动态绑定)。

二、向下转型

子类对象指向父类引用为向下转型

sonClass obj = (sonClass) fatherClass;

向下转型可以调用子类类型中所有的成员

父类引用对象指向的是子类对象,那么在向下转型的过程中是安全的,编译不会出错。

但若父类引用对象是父类本身,那么在向下转型的过程中是不安全的,编译不会出错,但是运行时会出现 Java 强制类型转换异常,一般使用 instance 来避免此类错误。

int和Integer的区别

1、Integer是int的包装类,int则是java的一种基本数据类型 2、Integer变量必须实例化后才能使用,而int变量不需要 3、Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值 4、Integer的默认值是null,int的默认值是0

延伸: 关于Integer和int的比较 1、由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。

Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.print(i == j); //false

2、Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)

Integer i = new Integer(100);
int j = 100;
System.out.print(i == j); //true

3、非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为 ①当变量值在-128~127之间时,非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同;②当变量值不在-128~127之间时,非new生成Integer变量时,java API中最终会按照new Integer(i)进行处理(参考下面第4条),最终两个Interger的地址同样是不相同的)

Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false

4、对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false

Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true
Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false

对于第4条的原因: java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100);,而java API中对Integer类型的valueOf的定义如下:

public static Integer valueOf(int i){
    assert IntegerCache.high >= 127;
    if (i >= IntegerCache.low && i <= IntegerCache.high){
        return IntegerCache.cache[i + (-IntegerCache.low)];
    }
    return new Integer(i);
}

java对于-128到127之间的数,会进行缓存。

如果有错误的地方,还请指正。

参考: http://blog.csdn.net/you23hai45/article/details/50734274 http://www.cnblogs.com/liuling/archive/2013/05/05/intAndInteger.html

转自:java面试题之int和Integer的区别 果冻迪迪

枚举类


  1. 构造器私有化
  2. 本类内部创建一组对象
  3. 对外暴露对象(用 public static final 修饰)
  4. 可以提供 get 方法
  5. 用 enum 关键字声明一个类时,默认会继承 Enum 类,而且是一个 final 类
  6. 常量名(参数) 等同于创建对象 , 多个可以用 ‘,’ 间隔,且必须放在类的首行
package EnumClass;

public class TestEnumClass {
    public static void main(String[] args) {

        Season[] season = Season.values();
        for (Season season1 : season) {
            System.out.println(season1);
        }

        Season sp = Season.valueOf("SPRING");
        System.out.println(sp);
    }
}

enum Season{
    SPRING("春天"), SUMMER("夏天"); 
    // 相当于 public static final SPRING = new Season("春天") 但是不能在 enum 修饰的类使用
    private String name;
    private Season (String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Season{" +
                "name='" + name + '\'' +
                '}';
    }
}

内部类

1. 局部内部类


package InnerClass;

public class LocalInnerClass {
    public static void main(String[] args) {
        Outer02 o2 = new Outer02();
        o2.f1();
    }
}

class Outer02 {
    private int i = 10;
    private void m2() {
        System.out.println("Outer02 m2()");
    }
    public void f1() {
       final class Inner02 {
            public void f2() {
                System.out.println(i);
                m2();
            }
        }
        // 代码块形式 声明局部内部类
//        {
//            class Inner03{
//
//            }
//        }
        Inner02 i2 = new Inner02();
        i2.f2();
    }
}
  1. 局部内部类是定义在外部类的局部位置,通常在方法
  2. 可以直接访问外部类的所有成员,包括私有的
  3. 不能添加修饰符,但是可以使用 final 修饰
  4. 作用域:仅仅在定义它的方法或代码块中
  5. 外部类在方法中可以创建内部类对象
  6. 外部其他类不能访问局部内部类
  7. 若外部类和局部内部类的成员重名时,默认遵循就近原则,若想访问外部类的成员,可以使用 来访问

2. 匿名内部类


需求:使用 IA 接口,并创建对象

  1. 传统方式:写一个类,实现该接口,并创建对象
  2. 需求是该类只是使用一次,后面不再使用,故可以使用匿名内部类来简化开发
interface IA{
    public abstract void cry();
}

// 底层会分配类名 外部类$1
//class XXXX implements IA{
//    @override
//    public void cry() {
//        System.out.println("老虎叫唤...");
//    }
//}

// tiger 的编译类型:IA
// tiger 的运行类型:匿名内部类

// JDK 底层在创建匿名内部类 外部类名$1 时,立马就创建了 外部类名$1 实例,并且把地址返回给tiger
// 匿名内部类使用一次,就不能再使用, 但这个对象 tiger 可以使用多次
IA tiger = new IA() {
    @override
    public void cry() {
        System.out.println("老虎叫唤...");
    }
}

  • 作用域:仅仅定义在外部类的方法或代码块中

  • new SuperType(construction parameter) {
        inner class methods and data
    }

    SuperType 是类,就需要扩展它;是接口,就需要实现它的方法。

最佳实践:作为实参传递

package InnerClass;

public class AnonymousInnerClass {
    public static void main(String[] args) {

        // 匿名内部类
//        f1(new IF(){
//          public void work() {
//                System.out.println("哈哈...");
//            }
//        });

        // lambda 表达式
        f1(() -> System.out.println("哈哈..."));
    }
    public static void f1(IF it) {
        it.work();
    }
}

interface IF{
    void work();
}

3. 成员内部类


  1. 定义在外部类的成员位置,并且没有 static 修饰
  2. 可以访问外部类的所有成员
  3. 可以添加任意访问修饰符,它的地位就是一个成员
  4. 作用域:为整个类体
  5. 外部其它类访问:
    • 外部类名.内部类名 对象名 = new 外部类名().new 内部类名();
    • 在外部类中,编写一个方法,可以返回 内部类对象
package InnerClass;

public class MemberInnerClass {
    public static void main(String[] args) {
        // 第一种
        Outer.Inner inner = new Outer().new Inner();
        inner.f1();
        // 第二种
        Outer outer = new Outer();
        outer.f2().f1();
    }
}

class Outer{
    private int i = 100;

    class Inner{
        public void f1() {
            System.out.println("哈哈" + i);
        }
    }
    public Inner f2() {
        return new Inner();
    }

}

4. 静态内部类


  1. 定义在外部类的成员位置,并且有 static 修饰
  2. 可以访问外部类的静态变量
  3. 作用域:整个类体
  4. 外部其它类访问:
    • 外部类名.内部类名 对象名 = new 外部类名.内部类名()
package InnerClass;

public class StaticInnerClass {
    public static void main(String[] args) {
        Outer01.Inner inner = new Outer01.Inner();
        inner.f1();
    }
}

class Outer01 {
    private static int i = 100;
    public static class Inner {
        public void f1() {
            System.out.println("哈哈");
        }
    }
    public void m1() {
        Inner inner = new Inner();
        inner.f1();
    }
}

三元操作符

类型转换规则:

  1. 若两个操作数不可转换,则不做转换,返回值为 Object 类型
  2. 若两个操作数是明确类型的表达式(比如变量),则按照正常的二进制数字来转换,int类型转换为long类型,long类型转换为float类型等。
  3. 若两个操作数中有一个是数字S,另外一个是表达式,且其类型标示为T,那么,若数字S在T的范围内,则转换为T类型;若S超出了T类型的范围,则T转换为S类型。
  4. 若两个操作数都是直接量数字,则返回值类型为范围较大者

事务

四个特性及对应子系统:


  • 原子性:安全性管理子系统
  • 一致性:完整性管理子系统
  • 隔离性:并发控制子系统
  • 持久性:恢复管理子系统

堆和栈

堆区:不存放基本类型和对象引用,只存放对象本身。(动态分配的变量)

栈区:存放基础数据类型的对象和自定义对象的引用。(函数参数和局部变量)

栈分为三部分:基本类型变量去、执行环境上下文、操作指令区(存放操作指令)


堆有年轻代(一个Eden区和两个survivor区组成),老年代,持久代。

  • 新创建的对象都在年轻代的Eden区;
  • 经过一次GC收集后,存活下来的会被复制到survivor区(一个满了,就全部移动到另外一个大的中,但要保证其中一个survivor为空);
  • 经过多次GC后,还存活的对象就被移到老年代了
  • 持久代就是Java虚拟机对方法区的一个具体实现,里面存放类信息,常量池,静态变量,方法等。
img
img

JVM 内存配置


-Xmx10240m: 最大堆

-Xms10240m: 最小堆

-Xmn5120m: 新生代

-XXSurvivorRatio=3: Eden:Survivor = 3

根据 Generation-Collection 算法,一般根据对象的生存周期将堆内存分为若干不同的区域,一般将新生代分为 Eden,两块 Survivor 。

新生代 大部分要回收,采用 Copying 算法,快!

老年代 大部分不需要回收,采用 Mark-Compact 算法

JDK1.8 中,永久代已经不存在,存储的类信息、编译的代码数据等已经移动到了 MetaSpace(元空间)中,它没有处于堆内存上,而是直接占用本地内存(NativeMemory)。img


JDK1.8 以后 移除了永久代用元空间取而代之,这时字符串常量池还在堆中,运行时常量池还在方法区,但方法区的实现从永久代变成了元空间(本地内存)

类的加载


  • 基本说明:
  1. 静态加载:编译时加载相关的类,如果没有则报错,依赖性太强。(new)
  2. 动态加载:运行时加载需要的类,如果运行时不用该类,即使不存在该类,则不报错,降低了依赖性。(反射)
  • 类加载时机
  1. 创建对象时(new) // 静态加载
  2. 当子类被加载时,父类也加载 // 静态加载
  3. 调用类中的静态成员时 // 静态加载
  4. 通过反射 // 动态加载
  • 类的加载过程图
image-20220411200721006
image-20220411200721006

加载(Loading):将字节码从不同的数据源转化为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class 对象,此过程由类加载器完成。

重复new同 一个 的对象也是只会 加载一次 这个 类 ,只是会多次执行实例化的步骤。 Java (二)—— JAVA类 只 载一次

链接(Linking):将类的二进制数据合并到 JRE 中

  • 验证:
  1. 目的:确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
  2. 包括:文件格式验证、元数据验证、字节码验证和符号引用验证
  3. 可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间
  • 准备:
  1. JVM 会在该阶段对静态变量,分配内存并默认初始化。这些变量所使用的内存都将在方法区中进行分配。
  • 解析:
  1. 虚拟机将常量池内的符号引用替换为直接引用(分配了内存故有地址)的过程

初始化(Initialization):JVM 负责对类进行初始化,主要指静态成员

  1. 真正执行类中定义的 Java 程序代码,执行 () 方法
  2. () 方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有 静态变量的赋值动作和 静态代码块中的语句,并进行合并。
  3. 虚拟机会保证一个类的 () 方法在多线程环境中被正确地 加锁、同步

实例:

public class B
{
    public static B t1 = new B();
    public static B t2 = new B();
    {
        System.out.println("构造块");
    }
    static
    {
        System.out.println("静态块");
    }
    public static void main(String[] args)
    
{
        B t = new B();
    }
}

首先 JVM 加载 B.class,初始化静态域,但是 t1 需要显式初始化(new),故执行非静态代码块 -> 构造方法(默认)(因为此时 JVM 认为 static 已经初始化)故输出”构造块“,t2 类似,输出“构造块”。此时静态变量已经全部被初始化,故初始化静态块(输出“静态块”),然后执行 main 方法,此时静态域已全被初始化,new,输出”构造块”。

类加载器


  1. 引导类加载器(bootstrap class loader) :用来加载 Java 的核心库,是用源代码实现的
  2. 扩展类加载器(extensions class loader):加载 Java 的扩展库
  3. 系统类加载器(system class loader):根据 Java 应用的类路径(CLASSPATH)来加载类
  4. tomcat 为每个 APP 创建一个 Loader,里面保存着此 WebApp 的 ClassLoader,需要加载 WebApp 下的类时,就取出 ClassLoader 来使用

类的加载顺序


  1. 父类静态变量和静态代码块; // 按声明顺序
  2. 子类静态变量和静态代码块;// 按声明顺序
  3. 父类普通变量和非静态代码块,父类构造函数; // 前两者(按声明顺序)优先级一般高于 构造函数
  4. 子类普通变量和非静态代码块,子类构造函数。// 前两者(按声明顺序)优先级一般高于 构造函数

1、一种是需要建立存储空间的。例如:int a 在声明的时候就已经建立了存储空间

2、另一种是不需要建立存储空间的。 例如:extern int a 其中变量a是在别的文件中定义的。

声明是向编译器介绍名字--标识符。它告诉编译器“这个函数或变量在某处可找到,它的模样象什么”。

而定义是说:“在这里建立变量”或“在这里建立函数”。它为名字分配存储空间。无论定义的是函数还是变量,编译器都要为它们在定义点分配存储空间。

把建立空间的声明成为“定义”,把不需要建立存储空间的成为“声明”

基本类型变量的声明和定义(初始化)是同时产生的;而对于对象来说,声明和定义是可分开的

引用自:https://blog.csdn.net/hi_boyI/article/details/78973155

继承

调用构造方法的原则

  • 若一个类的构造方法中第一条语句没有用 super 来调用父类的构造方法,编译器也会默认在构造方法中用 super() 调用父类的无参构造方法
  • 若父类中定义了有参构造方法,则 Java 系统不再提供默认的无参构造方法,因此在子类的构造方法中一定需要通过 super 显式调用父类有参构造方法。

可变参数


示例:

// nums 可以当作数组来用
public int f1(int... nums) {
    return nums.length();
}
// 与普通类型的参数放在一起是,可变参数需要放在最后
public void f2(String st1, double... nums) {
    
}

注意事项及使用细节:

  1. 可变参数的实参可以为 0 个或任意多个;
  2. 可变参数的实参可以为数组;
  3. 可变参数的本质就是数组;
  4. 可变参数可以和普通类型的参数一起放在形参列表,但必须保证可变参数在最后;
  5. 一个形参列表中只能出现一个可变参数。

IO流


一、文件基础知识:

1. 创建文件
  • new File(String pathname) // 根据路径构建一个 File 对象

  • new File(File parent, String child) // 根据父目录文件 + 子路径构建

  • new File(String parent, String child) // 根据 父目录 + 子路径构建

  • createNewFile() 创建新文件

2. FileWriter

使用后一定要 close() 或者 **flush()**,否则写入不到文件中,还存在于内存中(源码)

3. 文件拷贝
package IOStream;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class TextFileInputStream {
    public static void main(String[] args) {
        byte[] buf = new byte[10]; // 字节数组
        int realen = 0;
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;
        try {
            fileInputStream = new FileInputStream("F:\\news.txt");
            fileOutputStream = new FileOutputStream("F:\\new.txt");
            while  ((realen = fileInputStream.read(buf)) != -1) { // 若还有内容,则返回内容的字节数
                fileOutputStream.write(buf, 0, realen); // 一定要使用这个方法
                // fileOutputStream.write(buf) 不能使用这种方法
                // 例如:文件大小为 12 个字节,先读取 10 个字节,还剩两个,读到 buf 时,只有前两个更改,而后续 8 个字节还是原来的。
            }
        } catch (IOException e) {
        } finally { // 及时释放资源
            try {
                fileInputStream.close();
                fileOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

package IOStream;


import DecoratorPattern.BufferedReader_;

import java.io.*;

public class TestBufferedCopy {
    public static void main(String[] args) throws IOException {
        BufferedReader bufferedReader = null;
        BufferedWriter bufferedWriter = null;
        String readLine = null;
        try {
            bufferedReader = new BufferedReader(new FileReader("F:\\new.txt"));
            bufferedWriter = new BufferedWriter(new FileWriter("F:\\news.txt"));
            // 若目标文件不存在,则会创建一个
            while ((readLine = bufferedReader.readLine()) != null) {
                bufferedWriter.write(readLine);
                bufferedWriter.newLine(); // 需要插入与系统相关的换行
            }
        } catch (Exception e) {
        } finally {
            try {
                bufferedReader.close();
                bufferedWriter.close();
            } catch (Exception e) {
            }
        }
        System.out.println("拷贝成功");
    }
}

4. 二进制文件

BufferedInputStream 和 BufferedOutputStream 可以操作二进制文件(图片、视频、文本文件都行,字节是根本

二、节点流和处理流

  • 节点流可以从一个特定的数据源读写数据
  • 处理流(包装流)是“连接”在已存在的流之上,为程序提供更为强大的读写功能(装饰类模式

如:BufferedReader 里面有一个 Reader 类型的属性,可以接受各种 Reader 类型的流,从而对相应数据源进行操作(真正工作的就是节点流,关闭时只需关闭处理流,底层会自动关闭节点流)

三、对象流(处理流)

需求

  1. 将 Dog dog = new Dog("小黄", 3) 这个 dog 对象 保存到文件中,并且能从文件恢复
  2. 将 int num = 100, 这个 int 数据保存到文件中,并且能从文件中直接恢复 int 100
  3. 上述要求,就是能将 基本数据类型 和 对象 进行序列化和反序列化的操作

序列化和反序列化


序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。

序列化:将数据结构转换成二进制数据流的过程。序列化的数据方便在网络上传输和在硬盘上存储。

反序列化:与序列化相反,是将二进制数据流转换为易于处理和阅读的数据结构的过程。

本质是一种协议,一种数据格式,方便数据的存储和传输。


  1. 序列化就是在保存数据时,保存数据的值和数据类型
  2. 反序列化就是在恢复数据时,恢复数据的值和数据类型
  3. 需要让某个对象支持序列化机制,则必须让其是可序列化的,必须实现如下两个接口之一
  • Serializable 标记接口,没有方法
  • Externalizable 该接口有方法需要实现

解决(处理流/包装流)

  • ObjectOutputStream 提供序列化功能
  • ObjectInputStream 提供反序列化功能
package IOStream;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class TestObjectOutStream {
    public static void main(String[] args) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("F:\\new.dat"));
        oos.writeInt(100);
        oos.writeUTF("哈哈");
        oos.writeObject(new Dog("小白"3));
        oos.close();
    }
}

class Dog implements Serializable {
    private String name;
    private int age;
    public Dog() {}
    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

package IOStream;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class TextObjectInputStream {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("F:\\new.dat"));
        // 需要按序列化的顺序反序列化读取
        System.out.println(ois.readInt());
        System.out.println(ois.readUTF());
        System.out.println(ois.readObject());
    }
}

注意事项及细节说明

  1. 读写顺序要一致
  2. 要求实现序列化或反序列化的对象,需要实现 Serializable
  3. 序列化的类中建议添加 SerialVersionUID 属性(序列化的版本号),从而提高版本的兼容性

public static final long serialVersionUID = 1L; 当你给类增加新属性,它不会认为它是一个全新的类,而是一个修改版

  1. 序列化对象时,默认将里面所有的属性进行序列化,但除了 statictransient 修饰的成员
  2. 序列化对象时,要求里面所有属性的类型也需要实现序列化接口
  3. 序列化具备可继承性,也就是如果某类已经实现了 Serializable,那么它的子类也已经默认实现了Serializable 接口。

四、标准输入输出流

  • System.in 标准输入 编译类型:InputStream(父类) 运行类型:BufferedInputStream 默认设备:键盘

    • public final static InputStream in = null;
      InputStream in = new BufferedInputStream(System.in)
  • System.out 标准输出 PrintStream(父类)运行类型也为 PrintStream 默认设备:显示器

五、转换流

  • InputStreamReader
  • OutputStreamWriter
  • 文件乱码问题
package IOStream;

import java.io.*;

/**
 * 使用 InputStreamReader 解决中文乱码问题  文件为 UTF-8 编码
 * 将 FileInputStream 包装为 InputStreamReader 并指定编码 ”gbk"
 */

public class TestInputStreamReader {
    public static void main(String[] args) throws IOException {
        String filePath = "F:\\new.txt";
        InputStreamReader isr = new InputStreamReader(new FileInputStream(filePath), "gbk");
        BufferedReader br = new BufferedReader(isr);
        String rL = null;
        while ((rL = br.readLine()) != null) {
            System.out.println(rL);
        }
        br.close();
    }
}

六、打印流(处理流)

  • PrintStream
  • PrintWriter
  • 打印流只有输出流
package IOStream;

import java.io.FileNotFoundException;
import java.io.PrintStream;

public class TestPrintStream {
    public static void main(String[] args) throws FileNotFoundException {
        PrintStream out = System.out; 
        //    public final static PrintStream out = null;
        out.print("Hello, World!"); // 默认是显示设备

        System.setOut(new PrintStream("F:\\new.txt")); // 更改输出位置
        PrintStream out01 = System.out;
        out01.print("Hello, World !");
    }
}

package IOStream;

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

public class TestPrintWriter {
    public static void main(String[] args) throws IOException {
        PrintWriter pw = new PrintWriter(new FileWriter("F:\\new.txt"));
        pw.write("Hello, World!");
        pw.close(); // 真正写到文件中去 是在 close 方法中
    }
}

throws


后接类型(可多种)

  • 用来声明一个方法可能抛出所有的异常信息,表示出现异常的一种可能性,但并不一定会发生这些异常。
  • 通常在一个方法(或类)的声明处通过 throws 声明方法(或类)可能抛出的异常信息;
  • Java 总允许在方法的后面使用 throws 关键字对外声明该方法有可能发生异常,这样调用者在调用方法时,就明确地知道该方法有异常,那么在 main 方法中的调用者就必须对这个方法进行异常处理,否则编译不通过

throw


后接对象(一个)

  • 是指抛出的一个具体的异常类型,执行 throw 则一定抛出了某种异常对象;
  • 在方法(或类)内部通过 throw 声明一个具体的异常信息;
  • 需要用户自己捕获相关的异常 ,或者对其进行相关包装,最后将包装后的异常信息抛出。

若不处理隐式异常,则该异常会隐式上移交给调用该方法的方法,即默认异常处理方式

若不处理显式异常(throw 抛出),则必须在定义该方法时使用 throws 子句作显式异常抛弃声明

package Exception_;

public class TestException01 {
    public static void main(String[] args) {
        try {
            int ans = 102;
            if (ans < 0 || ans > 100) {
                throw new Exception("超出");
            }
        } catch (Exception e) {
            e.printStackTrace(); // 捕获后程序能继续执行
            // throw e;   则需要在方法头 声明 throws 抛出异常类型(异常抛回这个方法的调用者,程序也就运行到那里)
        }
        System.out.println("hh...");
    }
}

Object 的三种方法


  public boolean equals(Object obj) {
        return (this == obj);
    }
 public native int hashCode();
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
        // 类名 + @ + 以 对象的哈希码 为参数,返回其十六进制的无符号整数返回其字符串表示形式
    }

一般自定义需要重写这三种方法:

equals 重写为 比较对象内容,而 hashcode 需要跟着 equals 重写,因为它们一般用于协同判断两个对象是否相等,可以提高程序插入和查询的速度。且 Object 下的 hashcode 是根据对象自动生成的整数(根据对象的引用地址判断两者是否相同(与Object下的equals一致的判别方法),若改变了(equals)判别两个对象相等的方式,会导致 hashcode 出现异常,从而在用到两者的其他类上面会出错。

而 toString 方法 是为了将对象信息 以文字的形式呈现出来,便于查看,一般重写为返回 类名+[所有域的值]。

重写 equals 方法


import java.util.*;
public class Equals{
 private field1, field2;
 public boolean equals(Object other) // 重写 Object 类的 equals 方法
  if (this == other) return true// 优化 检测两者是否引用同一个对象

  if (other == nullreturn false// 若this 与 other 均为 null 也会返回 true 故需要此句

        // 判断 this 与 other 是否属于同一个类 
  if (getClass() != other.getClass()) return false
  // 若 equals 的语义在每个子类有所改动 就使用 getClass 检测
  // getClass 返回对象运行时所属的类  (此处用于判断是不是这个类)

  // if (!(other instanceof Equals)) return false;
  // 若所有的子类都拥有统一的语义 就用 instanceof 检测
  // instanceof 判断左边是否为右边的实例 须具有继承关系或同属一个类,且左边不能是基本数据类型
  // 类的实例包括本身的实例,以及所有直接或间接的实例

  Equals o = (Equals) other; // 将 other 转化为 Equals 类型

  return field1 == o.field1 && Objects.equals(field2, o.field2) && ...
  // 对所有需要比较的域 进行比较 == 用于比较基本类型域 Objects.equals() 用于比较对象域
  // Objects 是 Object 的工具类
  // 而 object 的 equals 方法 相当于 “==”

   Objects.equals 的源码
  // public static boolean equals(Object a, Object b) {
        // return (a == b) || (a != null && a.equals(b)); 
        // } //其中 a.equals(b) 为 Object 中的 equals 相当于"==" (需要保证 a 和 b 的类型相同(从几大基本数据类型的包装类的equals方法源码可以看出)
 }
}

面试问题:


1. 为什么重写 equals 方法要重写 hashCode 方法?

hashCode 方法用来计算哈希值获取索引位置,可以提高获取数组中元素的效率,但同时可能会发生哈希冲突(因为 hashCode 方法是通过一定的数学逻辑来计算出哈希码的),当发生哈希冲突时,我们就需要 equals 方法来判断两个对象是否相等。如果只重写了 hashCode 方法,那么哈希冲突发生时,即使两个对象相等,也不会判断为重复,进而导致数组里会存储一大堆重复对象。如果只重写了 equals 方法,那么两个相等的对象, hashCode 计算得到的哈希码,不一定相等,这样还是会造成重复元素的问题。

总结:hashCode 方法用来在最快时间内判断两个对象是否相等,并定位索引的位置,不过可能会出现误差,equals 方法用来判断两个对象是否"绝对"相等。hashCode 方法用来保证性能,equals 方法用来保证可靠性。

因为这两个方法是用来协同判断两个对象是否相等的,可以提高程序插入和查询的速度。若不重写 hashCode ,在某些场景下会出现异常。且重写才能保证不违背 hashCode 中“相同对象必须拥有相同的哈希值”。

例如:将两个相等的自定义对象存储在 Set 集合中,默认情况下,Set 集合会去判断两个对象的 hashCode 是否相同,而 Object 中的hashCode 对比的是两个对象的引用地址,所以结果是 false,就不用执行 equals 方法了,造成两个相等的对象没有去重。(而重写的equals 方法对比的是对象的所有属性)。

所以需要将 hashCode 重写:

// This is typically implemented by converting the internal address of the object into an integer
public int hashCode() {
    return Objects.hash(filed1, filed2, ...);
}

地址比较是通过计算对象的哈希值来比较的,hashCode 属于 Object 的本地方法,对象相等(地址相等) -> hashCode 相等, 对象不相等 -> hashCode 可能相等(哈希冲突)。

Objects 的 hash 方法


    /*
    * Objects.hash() 方法
    */

    public static int hash(Object... values) {
        return Arrays.hashCode(values);
    }
 /*
 * Arrays.hashCode() 方法
 */

    public static int hashCode(Object a[]) {
        if (a == null)
            return 0;

        int result = 1;

        for (Object element : a)
            result = 31 * result + (element == null ? 0 : element.hashCode());

        return result;
    }
 /*
 * Object.hashCode() 方法
 */

 public native int hashCode()// 返回对象的 hash 码(使用Xorshift随机数生成 java8)

泛型


好处:安全简单。在编译的时候检查类型安全,并且所有的强制转换都是自动的和隐式的,提高代码的重用率。

package GenericClass;

import java.time.LocalDate;

public class TestPair02  {
    public static void main(String[] args) {

        LocalDate[] birthday = {
                LocalDate.of(1999102),
                LocalDate.of(2000101),
                LocalDate.of(200110,21),
                LocalDate.of(2002101),
        };
        Pair<LocalDate> pair = Arraylag.minmax(birthday);
        System.out.println("min = " + pair.getFirst());
        System.out.println("max = " + pair.getSecond());
    }
}

class Arraylag {
    public static <T extends Comparable> Pair<T> minmax(T[] a) 
        // 类型变量的限定(将 T 限制为实现了 Comparable 接口)  泛型方法
        if (a == null || a.length == 0return null;
        T min = a[0];
        T max = a[0];
        for (int i=0; i<a.length; i++) {
            if (min.compareTo(a[i]) > 0) min = a[i];
            if (max.compareTo(a[i]) < 0) max = a[i];
        }
        return new Pair<>(min, max);
    }
}

类型擦除


Java 的泛型实现方式是类型擦除的伪泛型,在 Java 中,泛型只在程序源码中存在,编译后的字节码文件中泛型全部被擦除,替换为原来的裸类型,并在相应的位置插入了强制转型代码。

无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型(无限定的变量用 Object)(若有多个则用第一个限定的类型变量来替换,且应将标签(tagging)接口放在边界列表的末尾。

约束与局限性


  1. 不能用基本类型实例化类型参数

原因:类型擦除。例如:Pair 擦除之后,Pair 类含有 Object 类型的域,而 Object 不能存储 double 值。

  1. 运行时类型查询只适用于原始类型

原因:虚拟机中的对象总有一个特定的非泛型类型,因此,所有的类型查询只产生原始类型。(instanceof、getClass()方法 )

  1. 不能创建参数化类型的数组

原因:Pair [] table = new Pair [10] // Error

擦除后,table 的类型为 Pair[],可以把他转换为 Object[] Object[] objarray = table

数组和会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个 ArrayStoreException异常。

只是不允许创建这些数组,而声明类型为 Pair [] 的变量仍是合法的,不能用 new Pair [10] 初始化这个变量。

若需要收集参数化类型对象,使用 ArrayList:ArrayList<Pair >

  1. Varargs 警告

原因:向参数个数可变的方法传递一个泛型类型的实例。

为了调用这个方法,Java 虚拟机必须建立一个参数化类型的数组,对于这种情况,规则有所放松,只会得到一个警告,而不是错误。

  • 增加注解 @SuppressWarnings("unchecked") 来抑制这个警告
  • 使用 @SafeVarargs 直接标注方法(对于只需要读取参数数组元素的所有方法,都可以使用这个注解,仅限于最常见的用例)
  1. 不能实例化类型变量

原因:类型擦除。不能使用像 new T(),new T[] 或 T.class 这样的表达式。无法确定 T 的类型,就无法在内存中开空间。

Java SE 8 后最好的解决方法就是让调用者提供一个构造器表达式。

  1. 不能构造泛型数组

数组本身也有类型,用来监控存储在虚拟机中的数组,这个类型会被擦除。

  1. 泛型类的静态上下文中类型变量无效

不能在静态域或方法中引用类型变量。

原因:静态时和类相关的,在类加载时,对象还没有创建(泛型类的类型是在创建对象时就确定的),故 JVM 不知道泛型的具体类型。

  1. 不能抛出或捕获泛型类的实例
  2. 可以消除对受查异常的检查
try {
    do work
catch (Throwable t) {
    Block.<RuntimeException>throwAs(t);
}

这段代码会把所有的异常都转换为编译器所认为的非受查异常。

package GenericClass;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class TestExceptionBlock {
    public static void main(String[] args) {
        new block() { // 匿名内部类
            public void body() throws Exception {
                Scanner in = new Scanner(new File("ququx"), StandardCharsets.UTF_8);
                while (in.hasNext()) {
                    System.out.println(in.next());
                }
            }
        }.toThread().start();
    }
}

abstract class block {
    public abstract void body() throws Exception;
    public Thread toThread() {
        return new Thread(()-> { // lambda 表达式
                try {
                    body();
                } catch (Throwable t) {
                    block.throwAs(t);
                }
        });
    }
    
    @SuppressWarnings("unchecked")
    public static <T extends Throwable> void throwAs(Throwable e) throws T {
        throw (T) e;
    }
}

运行这段程序,会得到一个栈轨迹(假设没有 ququx 文件)

正常情况下,必须捕获线程 run 方法中的所有受查异常,把它们“包装”到非受查异常中。而在这里没有进行包装,只是抛出异常,并”哄骗“编译器,让它认为这不是一个受查异常。

通过使用泛型类、擦除和 @SuppressWarnings 注解,就能消除 Java 类型系统的部分基本限制。

  1. 注意擦除后的冲突

例如:

public class Pair<T{
    public boolean equals(T value) {
        return first.equals(value) && second.equals(value);
    }
}

考虑一个 Pair ,从概念上讲他有两个 equals 方法:

boolean equals(String) // defined in Pair

boolean equals(Object) // inherited from Object

前者方法擦除就是 后者,与 Object.equals 方法冲突,补救:重写命名引发错误的方法。

泛型规范说明:要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。

泛型类型的继承规则


image-20220409155310525
image-20220409155310525
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair<Employee> employeeBuddies = managerBuddies;
// illgal, no relationship
Manager[] managerBuddies = {ceo, cfo};
Employee[] employeeBuddies = managerBuddies; // OK

可以将一个 Manager[] 数组赋给一个类型为 Employee[] 的变量。

然而,数组带有特别的保护,如果试图将一个低级别的雇员存储到 employeeBuddies[0],虚拟机将会抛出 ArrayStoreException 异常。

通配符类型

Pair<? extends Employee>

image-20220409161306526
image-20220409161306526
extends Employee getFirst()
void setFirst (? extends Employee)

不能调用 setFirst 方法。编译器只知道需要某个 Employee 的子类型,但不知道具体是什么类型。它拒绝传递任何特定的类型。

而使用 getFirst 就不存在这个问题,将 getFirst 的返回值赋给一个 Employee 的引用完全合法。

Pair<? super Manger>

image-20220409172219806
image-20220409172219806
void setFirst(? super Manager)
super Manager getFirst()

编译器无法知道 setFirst 方法的具体类型,因此调用这个方法时不能接受类型为 Employee 或 Object 的参数,只能传递 Manager 类型的对象,或者某个子类型对象。

如果调用 getFirst,不能保证返回对象的类型,只能把它赋给一个 Object。

In short,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型类对象读取。

多线程


1. synchronized 用法:
  • 修饰一个:作用范围:synchronized 后面括号括起来的部分,对象:这个类的所有对象
class ClassName {
   public void method() {
      synchronized(ClassName.class// 对象同步
         // todo
      }
   }
}
  • 修饰一个方法:称为同步方法,范围:整个方法,对象:调用这个方法的对象
  1. 定义接口方法不能使用 synchronized

  2. 构造方法不能使用 synchronized,但可以使用 synchronized 代码块来进行同步

  3. synchronized 关键字不能被继承。(如果子类覆写了父类被 synchronized 修饰的方法,那么子类的该方法只要没有 synchronized 关键字,就默认没有同步。

  • 修饰一个静态的方法:范围:整个方法,对象:这个类的所有对象
  • 修饰一个代码块:同步语句块,{}括起来的代码块,对象:调用这个代码块的对象
public class Test {
    private synchronized void a() // 同步非静态方法 锁当前类的实例对象
    }
    private void b() {
        synchronized (this) { // 同步代码块  锁的是()内的  this当前类的对象(只对当前类的对象上锁)
        }
    }
    private synchronized static void c() // 同步静态方法 锁的是 类对象
    }
    private void d() {
        synchronized (Test.class// 同步代码块 锁的是()内的  Test.class(锁住的是整个类)
        }
    }
}
package Others_;

public class Test05 {
    public static void main(String[] args) {
        Person you = new Person();
        Person he = new Person();
        //不同对象,在类锁下会同步。
        Thread you1 = new Thread(you, "苇名一心");
        Thread he1 = new Thread(he,"苇名弦一郎");

        you1.start();
        he1.start();
    }

}

class Person implements Runnable
{
    public void run() {
        synchronized (Person.class{
            for(int i = 0; i <= 10; i++) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"放巴之雷");
            }
        }
    }
}

package Others_;

public class Test05 {
    public static void main(String[] args) {
        Person you = new Person();
        // 同一对象,在 this 锁下会同步
        Thread you1 = new Thread(you, "苇名一心");
        Thread you2 = new Thread(you,"苇名弦一郎");

        you1.start();
        you2.start();
    }

}

class Person implements Runnable
{
    public void run() {
        synchronized (this) {
            for(int i = 0; i <= 10; i++) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"放巴之雷");
            }
        }
    }
}

2. 创建线程的两种方式
  • 继承 Thread 类(实现了 Runnable 接口的 run 方法),重写 run 方法
  • 实现 Runnable 接口,重写 run 方法
package Multi_Thread;

public class Test01_ {
    public static void main(String[] args) throws InterruptedException {
        Cat cat = new Cat();
        cat.start(); // 将线程变成可运行状态,但真正的执行 需要看 CPU 的调度
        // cat.run() 只是一个普通的调用方法 不会启动线程
        // 启动一个线程
        /*
            public synchronized void start() {
            start0();   // 关键
            }
            private native void start0();
            // 本地方法 由 JVM 去调用,底层由 C/C++ 实现
         */

        // main 为主线程  在这里开启一个线程,不会阻塞,两个线程交替执行。

        Dog dog = new Dog();
        Thread thread = new Thread(dog); // 【代理模式】
        thread.start();

        for (int i=0; i<55; i++) {
            System.out.println("主线程运行中..." + Thread.currentThread().getName());
            Thread.sleep(1000);
        }
        cat.setLoop(false); // 通知 线程 Cat 结束
    }
}

class Cat extends Thread{
    public boolean loop = true;
    @Override
    public void run() {
        int times = 0;
        while (loop) {
            System.out.println("小猫咪" + (++times) + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (times == 70break;
        }
    }
    public setLoop(boolean loop) {
        this.loop = loop;
    }
}

class Animal {};
class Dog extends Animal implements Runnable {  // Runnable 只有 run 方法
    @Override
    public void run() {
        System.out.println("小狗汪汪叫...");
    }
}

class ThreadProxy extends Thread {   // 模拟一个极简的 Thread
    private Runnable target = null// Runnable 属性
    public ThreadProxy(Runnable target) {
        this.target = target;
    }
    public void run() {
        if (target != null) {
            target.run();
        }
    }
    public void start() {
        start0();
    }
    public void start0() {
        run();
    }
}
3. 几种常用方法
  • interrupt 方法:中断线程
package Multi_Thread;

public class Test02_ {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        t.start();

        for(int i=0; i<5; i++) {
            Thread.sleep(1000);
            System.out.println("喝水" + i);
        }
        t.interrupt(); // 中断 t 的休眠
    }
}

class T extends Thread {
    @Override
    public void run() {
        while (true) {
            for (int i=0; i<50; i++) {
                System.out.println("吃饭中..." + i);
            }
            try {
                System.out.println("休眠中~~~");
                Thread.sleep(20000);
            } catch (InterruptedException e) { // 捕获中断异常,可以写自己的业务代码
                System.out.println("被中断了...");
            }
        }
    }
}
  • yield 方法:线程的礼让。让出 CPU,让其他线程执行,但礼让的时间不确定,所以不一定礼让成功。(取决于 CPU 的调度,资源)
  • join 方法:线程的插队。在一个线程 A 中,调用另外一个线程 B 的 join() 方法,则会先执行完线程 B 的所有任务。
4. 用户线程和守护线程
  1. 用户线程:也叫工作线程,当线程的任务执行完或以通知方式结束
  2. 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
  3. 常见的守护线程:垃圾回收机制
5. Thread 的六种状态

public static enum Thread.State extends Enum<Thread.State>

  • NEW 尚未启动的线程处于此状态
  • RUNNABLE 在 JAVA 虚拟机中执行的线程处于此状态 (Reading Running)
  • BLOCKED 被阻塞等待监视器锁定的线程处于此状态
  • WAITING 正在等待另一个线程执行特定动作的线程处于此状态
  • TIMED_WAITING 正在等待另一个线程执行动作到达指定等待时间的线程处于此状态
  • TERMINATED 已退出的线程处于此状态
image-20220413230104064
image-20220413230104064
5. 互斥锁
  • 每个对象都对应一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程来访问该对象。
  • 同步方法(非静态的)的锁可以为 this(需要保证是同一个),也可以为其他对象(需要是同一对象),要求多个线程的对象锁是同一个
  • 同步方法(静态的)的锁为当前类的对象
6. 死锁
  • 多个线程占用了对方的资源,但不肯相让,导致了死锁。
package Multi_Thread;

public class DeadLockDemo {
    public static void main(String[] args) {
        Demo demo1 = new Demo(true);
        Demo demo2 = new Demo(false);
        demo1.start();
        demo2.start();
    }
}

class Demo extends Thread{
    static Object o1 = new Object();
    static Object o2 = new Object();
    private boolean loop;
    public Demo(boolean loop) {
        this.loop = loop;
    }
    public void run() {
        if (loop) {
            synchronized (o1) { // 拿到了 o1  如果拿不到 o2 就会死锁
                System.out.println(Thread.currentThread().getName() + " 拿到 o1");
                synchronized (o2) {
                    System.out.println(Thread.currentThread().getName() + " 拿到 o2");
                }
            }
        } else {
            synchronized (o2) { // 拿到了 o2  如果拿不到 o1 就会死锁
                System.out.println(Thread.currentThread().getName() + " 拿到 o2");
                synchronized (o1) {
                    System.out.println(Thread.currentThread().getName() + " 拿到 o1");
                }
            }
        }
    }
}

7. 释放锁
  • 当前线程的同步方法、同步代码块执行结束
  • 当前线程在同步方法、同步代码块遇到了 break、return
  • 当前线程在同步方法、同步代码块中出现了未处理的 Error 或 Exception,导致异常结束
  • 当前线程在同步方法、同步代码块中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁

下列操作不会释放锁:

  • 线程执行同步方法或同步代码块时,程序调用 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行,不会释放锁

  • 线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁。(suspend() 方法 和 resume() 方法不再推荐使用)

  • 解释说明:

    • sleep 会使当前线程睡眠指定时间不释放锁
    • yield 会使当前线程重回到可执行状态,等待 cpu 的调度,不释放锁
      • yield()只能使同优先级或更高优先级的线程有执行的机会。
    • wait 会使当前线程回到线程池中等待,释放锁,当被其他线程使用 notify,notifyAll 唤醒时进入可执行状态
      • 当前线程必须拥有当前对象锁。如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常。
      • 唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常。
      • wait()和notify()必须在synchronized函数或synchronized block中进行调用。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。
    • 当前线程调用 某线程.join() 时会使当前线程等待某线程执行完毕再结束,底层调用了 wait,释放锁
img
img

集合类

img
img

Collection

public interface Collection<Eextends Iterable<E{
    //Collection 接口继承了 Iterable 接口
}

public class Collections {
    private Collections() {
    }
}
// Collections 类是不能被实例化的,因为它的构造方法为私有方法,且里面的方法都为静态方法
image-20220418205005545
image-20220418205005545

一、List


1. 遍历

package Collections_;

import java.util.ArrayList;
import java.util.Iterator;

public class Traverse_ {
    public static void main(String[] args) {
        ArrayList aL = new ArrayList();
        aL.add("西游记");
        aL.add("三国演义");
        aL.add("水浒传");
        Iterator it = aL.iterator();

        while (it.hasNext()) {
            System.out.println(it.next());
        }

        // 底层也是 Iterator  hasNext()  next()
        for (Object o : aL) {
            System.out.println(o);
        }
        
        for (int i=0; i<aL.size(); i++) {
            system.out.println(aL.get(i));
        }
    }
}

2. ArrayList

  • 可以加入 null,且可以重复 (里面是对象)
  • 是由数组来实现数据存储的
  • 线程不安全(执行效率高),故多线程的情况下,不建议使用 ArrayList
底层结构和源码分析(add() 方法 和 扩容机制)
  • ArrayList 中维护了一个 Object 类型的数组 elementData

    transient(瞬间,短暂的) Object[] elementData 表示该属性不会被序列化

  • 当创建 ArratList 对象时,如果使用的是无参构造器,则初始 elementData 容量为0,第一次添加,则扩容 elementData 为10若再次扩容,则扩容其 1.5 倍

  • 若使用的是指定大小的构造器,则初始 elementData 容量为指定大小,若需要扩容,直接扩容到 1.5 倍

  1. 创建一个空数组
  • 无参构造器
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 默认为空数组,第一次扩容到 10
}
  • 有参构造器
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity]; // 直接创建 传入的初始容量大小的 数组
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
  1. 装箱(int -> Integer)
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
  1. add() 方法
    • minCapacity 为需要的最小容量
image-20220415145006700
  1. ensureCapacityInternal() 方法
image-20220415145119445
  1. calculateCapacity() 方法

    • 若 数组为空 则返回 minCapacity 和 默认容量(10)的最大值,第一次即为 10

    • 否则返回 minCapacity

image-20220415145233289
  1. ensureExplicitCapacity() 方法 (确定是否需要扩容)

    • modCount 记录修改次数

    • 若 minCapacity > 数组长度,则进入扩容机制

image-20220415145505645
  1. grow() 方法

    • 第一次 oldCapacity 为 0,newCapacity 也为 0,则进入第一个 if 语句, newCapacity 被赋值为 10,然后采用 Arrays.copyOf 方法将原数组的内容复制到扩容后的数组。

    • 后续扩容,则直接扩容至原来的 1.5 倍

image-20220415145803934

3. Vector

  • 底层是一个对象数组,protected Object[] elementData
  • Vector 是线程同步的,即线程安全(操作方法带有 synchronized)
  • 在开发中,需要线程同步安全时,考虑用 Vector
底层结构和源码分析 (add() 方法 和 扩容机制)
  1. 创建数组
  • 无参构造器(初始化容量为10)
image-20220415155927516 image-20220415160009713
  • 有参构造器
image-20220415160543453
  1. 装箱
image-20220415160156410
image-20220415160156410
  1. add() 方法
image-20220415160222269
image-20220415160222269
  1. ensureCapacityHelper() 方法 (确定是否需要扩容)
image-20220415160301466
  1. grow() 方法 (一般 capacityIncrement 为 0,则扩容两倍)
image-20220415160344593
image-20220415160344593

关键算法:

int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity); 
// capacityIncrement 初始化为0(构器函数中)

4. LinkedList

  • LinkedList 底层实现了双向链表双端队列特点
  • 可以添加任意元素(元素可重复),包括 null
  • 线程不安全
  • 底层维护了一个 双向链表
  • LinkedList 维护了两个属性 first 和 last,分别指向首节点和尾节点
  • 每个节点(Node 对象),里面又维护了 prev、next、item三个属性,其中通过 prev 指向前一个,next 指向后一个节点。
  • LinkedList 元素的添加和删除,不是通过数组实现的,相对来说效率较高
底层结构和源码分析
1. add() 方法
  • 初始化 first = null,last = null
public LinkedList() {    
}
  • add
public boolean add(E e) {
    linkLast(e);
    return true;
}
void linkLast(E e) // 创建一个新节点 添加到末尾
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null); 
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
2. remove() 方法
  • remove()
public E remove() {
 return removeFirst();
}
  • removeFirst()
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
    throw new NoSuchElementException();
    return unlinkFirst(f);
}
  • unlinkFirst()
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null// help GC
    first = next;
    if (next == null)
      last = null;
    else
     next.prev = null;
    size--;
    modCount++;
    return element;
}

二、Set


  • 无序(不保存添加的顺序,添加和取出的顺序可能不同),没有索引
  • 不允许重复元素,所以最多包含一个 null

1. HashSet

  • HashSet 底层其实是 HashMap(数组+链表+红黑树(一定条件下转化为红黑树))
  • 可以存放一个 null 值
  • HashSet 不保证元素是有序的,取决于 hash 后,再确定索引的结果(不保证元素的添加顺序和取出顺序一致)
  • 不允许添加重复的元素
底层结构和源码分析
  • 添加一个元素时,先得到 hash 值,会转成 索引值
  • 找到存储数据表 table,看这个索引位置是否已经存放了元素
  • 若没有,则直接加入
  • 若有,调用 equals 方法依次比较链表的元素,若相同,就放弃添加,若都不相同,则添加到最后
  • 在 Java8 中,若一条链表的元素个数超过 TREEIFY_THRESHOLD(默认是 8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认64)就会进行树化(红黑树) (防止用户自己实现了不好的哈希算法时,导致链表过长,从而导致查询效率低)
  • 若一条链表的元素 小于等于6 时,树结构会还原成链表形式

添加元素:

  1. 先获取元素的哈希值(hash方法 -> hashCode 方法)。

  2. 对哈希值进行运算,得出一个索引值,即为要存放在哈希表中的位置号

    • 如果该位置上没有其他元素,则直接存放

    • 若该位置上有其他元素,则需要进行比较(其中需要用到 equals 方法)

    • if (p.hash == hash &&
          ((k = p.key) == key || (key != null && key.equals(k))))
          e = p;
1. 构造函数
public HashSet() {
    map = new HashMap<>(); // 底层就是 HashMap
}
 public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
     // Constructs an empty HashMap with the default initial capacity (16) and the default load factor (0.75)
    }
2. add 方法
public boolean add(E e) {
 return map.put(e, PRESENT)==null;
    // PRESENT: private static final Object PRESENT = new Object();
    // Dummy(虚拟的) value to associate with an Object in the backing Map
    // Returns:true if the set did not already contain the specified element
    // Returns:若这个set集合中没有包含 e 这个元素,则返回 true,即添加成功
}
public V put(K key, V value) // 添加键值对
    return putVal(hash(key), key, value, falsetrue);
}
static final int hash(Object key) // 计算 key 对应的 hash值 -> 索引
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 
    // 依据 key 的运行类型去调用对应的 hashCode() 方法
}
// Object 的 hashCode 方法
// 
public native int hashCode();
// 返回对象的 hash 码(使用Xorshift随机数生成? java8)

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict)
 
{
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    // 1. 判断 table(HashMap 的一个数组,类型为 Node<K,V>[]) 是否为空 或 长度为 0
    // 若为真,则进入 resize() 方法,第一次扩容到 16 个空间。
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    
    // 2. 根据 key 得到了 hash ,从而去计算 key 在 table 下对应的索引值
    // 并把这个位置的对象 赋值给 p,并判断 p 是否为 null
    // 若 p 为空,则表示该位置没有数据,就新建一个 Node
    // (注意:索引位置相同,不代表 hash 值也相同,故后续else语句还需要判断 hash值)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 3. 若不为空,进入 else 代码块
    else {
        // 3.1
        // 局部变量声明:在需要的地方进行声明
        Node<K,V> e; K k;
        // 若满足以下两个条件:
        // (1) 添加对象的hash 与 对应索引位置的链表第一个对象p的hash值相等
        // (2) 添加对象的key 与p的key相等,或 key不为空,且两者调用equals方法返回真
        // 则两者被判定为相同 不添加
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 3.2
        // 若 p 是 TreeNode 的实例,则进入 红黑树的 putTreeVal 方法
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 3.3
        // 若该索引位置的第一个元素与添加对象不相等,则
        // 将该索引位置的链表上的其他元素依次与添加对象进行比较
        else {
            for (int binCount = 0; ; ++binCount) {
                // 3.3.1
                // 若下一个元素为空,则创建一个添加对象的Node,并挂在链表末尾,然后退出
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 若 链表上的元素超过 8个,则进入 treeifyBin 方法
                    // 首先会进行如下判断,若 table 长度小于 64,则进行扩容,不会树化
                    // (注意:table 也不是会无限扩大的)
                    // if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
           //   resize();
                    if (binCount >= TREEIFY_THRESHOLD - 1// -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 3.3.2
                // 与3.1 一样 判断该元素是否与添加元素相同
                // 若有相同的,则直接退出
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e; // 移到链表的下一个元素
            }
        }
        // 3.3.3 HashMap 若添加重复的 key,则会用新的 Value值 替换掉旧的 Value值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 3.4
    ++modCount; // 修改次数
    // 若 table的size(元素的个数,包括链表的元素) 大于 边界,则进行扩容
    if (++size > threshold) 
        resize();
    afterNodeInsertion(evict); // 空方法
    // void afterNodeInsertion(boolean evict) { } 
    // Callbacks to allow LinkedHashMap post-actions
    return null;
}
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// initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY; // 第一次容量为 16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        // 边界值为 16*0.75 = 12
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = 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 { // preserve order
                    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;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab; // 返回新建的 table
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}
3. 为什么需要先计算 hash 值,而不直接equals 比较值是否相同?
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))

因为 hash 算法是二进制算法,速度很快,如若两者的 hash 值不同,则可以直接判断出不同,而不用在用 equals 进行比较,加快了存储效率。(同时也告诫程序员,用到 hashcode 和 equals,两者都需要重写,不然会导致存储重复元素)

2. LinkedHashSet


  • LinkedHashSet 是 HashSet 的子类
  • LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个 数组+双向链表,还有头结点和尾结点
  • LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序,使得元素看起来是以插入顺序保存的,故当遍历它时,可以按其添加顺序输出。
  • LinkedHashSet 不允许添加重复元素
  • image-20220418155544366
底层结构与源码分析
1. 构造函数
public LinkedHashSet() {
    super(16, .75ftrue);
    // initialCapacity: 16, loadFactor: 0.75, dummy: true
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}
2. add() 方法

与 HashSet 类似,但是 table 存的不是 Node 而是 Entry。

table 是 HashMap$Node 类型的

内部元素LinkedHashMap$Entry 类型的

static class Entry<K,Vextends HashMap.Node<K,V// 内部类继承(静态内部类)
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    // 静态内部类 创建对象实例
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}
// 将元素链接到 双向链表上
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

3. TreeSet

特点

  • 底层是 红黑树 数据结构(一颗自平衡的排序二叉树)

  • 可以进行排序(但采用无参构造器,仍然是无序的)

  • 可以在构造器中传入一个 实现 Comparator 的匿名内部类(函数式接口/Lambda表达式)

  • 内部有个 root 是 TreeMap$Entry 类型的

package Collections;

import java.util.Comparator;
import java.util.TreeSet;

public class TreeSet_ {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((String)o1).length() - ((String)o2).length();
            }
        });
        treeSet.add("abasd");
        treeSet.add("SDF");
        treeSet.add("asdasdasd");
        treeSet.add("a");
        treeSet.add("sd");
        treeSet.add("fz");

        System.out.println(treeSet);//  [a, sd, SDF, abasd, asdasdasd]
    }
}

1. 构造函数
public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}
private final Comparator<? super K> comparator;
2. add() 方法
public boolean add(E e) {
    return m.put(e, PRESENT)==null;
}
public V put(K key, V value) {
    // 每次都将 根赋值给 t,从根节点开始
    Entry<K,V> t = root;
    // 得到根节点
    if (t == null) {
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                // 若 cmp 相等,则将其 value 值替换(但 TreeSet 的 value 为 PRESENT 定值)
                return t.setValue(value);
        } while (t != null);
    }
    else {
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);
    // 若 cmp 小于 0,则将其左结点赋值为 e
    // 反之,则将其右结点赋值为 e
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

Map

image-20220414164812953
image-20220414164812953

特点

  • Map 用于保存具有映射关系的数据 Key—Value
  • Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中
  • Map 中的 key 不允许重复,若重复 key,等价于替换,Map 中的 value 可以重复
  • Map 的 key 可以为 null,value 也可以为 null,但 key 为 null,只能有一个,而 value 为 null 可以有多个
  • 常用 String 类作为 Map 的 key
  • key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value
  • 一对 k-v 是放在一个 HashMap#Node 中的,而且 Node 实现了 Entry 接口

一、HashMap

底层结构和源码分析
image-20220417150556088
image-20220417150556088

KeySet 和 Collection 里面的元素只是指向 Key 和 Value

table 是 HashMap$Node[] 类型的

1. add() 方法
  • 与 HashSet 类似

  • static class Node<K,Vimplements Map.Entry<K,V>
  • package Collections;

    import java.util.HashMap;
    import java.util.Map;

    @SuppressWarnings({"all"})
    public class Map_ {
        public static void main(String[] args) {
            Map map = new HashMap();
            map.put("NO.1""jack");
            map.put("NO.2""jamki");
            
            // 1. k-v 最后是 HashMap$Node node = new Node(hash,key,value,null)
            //     static class Node<K,V> implements Map.Entry<K,V>
            
            // 2. k-v 为了方便程序员的遍历,还会创建 EntrySet 集合
            // 该集合存放的元素类型为 Entry,而一个 Entry 对象就有k和v,
            // EntrySet<Entry<K,V>> 即 transient Set<Map.Entry<K,V>> entrySet
            
            // 3. entrySet 中,定义的类型是 Map.Entry
            // 但是实际上存放的还是 HashMap$Node (这里指向 table 中的 Node 元素)
            // 这是因为 static class Node<K,V> implements Map.Entry<K,V>
            
            // 4. 当把 HashMap$Node 对象存放到 entrySet 中,就方便我们的遍历
            // 因为 entrySet 提供了两个重要方法
            // (1) K getKey();
            // (2) V getValue();
            
            Set set = map.entrySet();
            for (Object obj : set) {
                Map.Entry entry = (Map.Entry) obj; // 向下转型
                System.out.println(entry.getKey() + " " + entry.getValue());
                
            // keySet 是专门存放 Key 的(指向 table 中的 key,并没创建新的东西)
            // 运行类型是 HashMap$KeySet
            // final class KeySet extends AbstractSet<K>
            Set set1 = map.keySet();
            System.out.println(set1.getClass());
            // values 是专门存放 values 的(指向 table 中的 value,并没创建新的东西)
            // 运行类型是 HashMap$Values
            // final class Values extends AbstractCollection<V>
            Collection values = map.values();
            System.out.println(values.getClass());
        }
    }

  • public String toString() // entrySet 的迭代器
        Iterator<Entry<K,V>> i = entrySet().iterator();
        if (! i.hasNext())
            return "{}";

        StringBuilder sb = new StringBuilder();
        sb.append('{');
        for (;;) {
            Entry<K,V> e = i.next();
            K key = e.getKey();
            V value = e.getValue();
            sb.append(key   == this ? "(this Map)" : key);
            sb.append('=');
            sb.append(value == this ? "(this Map)" : value);
            if (! i.hasNext())
                return sb.append('}').toString();
            sb.append(',').append(' ');
        }
    }
    public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }
2. 三组遍历方式
package Collections;

import java.util.*;

public class MapTraverse {
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("小白"5);
        map.put("小黑"6);
        map.put("小黄"7);
        map.put("小红"8);
        map.put("小蓝"9);

        // 第一组:获得 keySet,里面存放的是指向 key 的元素
        Set set = map.keySet();
        // (1) 增强 for
        for(Object key : set) {
  // System.out.println(key.getClass());
            System.out.println(key + " " + map.get(key));
        }
        // (2) 迭代器
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object key = iterator.next();
            System.out.println(key + " " + map.get(key));
        }

        // 第二组:获得 values,里面存放的是 指向 value 的元素
        Collection collection = map.values();
        // (1) 增强 for
        for (Object value : collection) {
            System.out.println(value);
        }
        // (2) 迭代器
        Iterator iterator1 = collection.iterator();
        while (iterator1.hasNext()) {
            Object value = iterator1.next();
            System.out.println(value);
        }

        // 第三组:获得 entrySet,里面存放的是 Entry<K,V>
        Set set1 = map.entrySet();
        // (1) 增强 for
        for (Object obj : set1) {
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
        // (2) 迭代器
        Iterator iterator2 = set1.iterator();
        while (iterator2.hasNext()) {
            Object obj = iterator2.next();
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
    }
}

3. HashMap 的 entrySet 是怎么得到 Node 值的
Set set1 = map.entrySet(); // (1)
//  增强 for
for (Object obj : set1) { // 这一步 其实也就是底层也是用 迭代器 实现的
    Map.Entry entry = (Map.Entry) obj;
    System.out.println(entry.getKey() + " " + entry.getValue());
}

实际上是通过迭代器获得 Node 的

  • public Set<Map.Entry<K,V>> entrySet() { // (1) 创建对象实例 new EntrySet()
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }
  • public final Iterator<Map.Entry<K,V>> iterator() {
        return new EntryIterator(); // 创建迭代器对象实例 EntryIterator 
    }
  • final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> 
    {
        public final Map.Entry<K,V> next() return nextNode(); } 
        // nextNode() 方法 会返回 Node结点
    }
    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        // 从 索引0 开始找,找到第一个不为 空的对象
        if (t != null && size > 0) { // advance to first entry
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }
    public final boolean hasNext() {
        return next != null;
    }
    • **调用 public final Map.Entry<K,V> next() { return nextNode(); **
    final Node<K,V> nextNode() {  // 此方法就返回了 Node对象
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        
        /* 若当前结点的 next 等于空  且  table 不为空
           则 在 table 中找到下一个 不为空的对象
           否则 next 就是当前索引位置 所在链表的下一个结点
        */

        if ((next = (current = e).next) == null && (t = table) != null) {
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }
4. 为什么要转成红黑树?

链表取元素是从头结点一直遍历到对应的结点,这个过程的复杂度是O(N) ,而红黑树基于二叉树的结构,查找元素的复杂度为O(logN) ,所以,当元素个数过多时,用红黑树存储可以提高搜索的效率。

5. 为什么不直接使用红黑树?

这其实是基于空间和时间平衡的考虑,单个 TreeNode 需要占用的空间大约是普通 Node 的两倍,所以只有当包含足够多的 Nodes 时才会转成 TreeNodes

6. 为什么树化标准是 8 个?

如果 hashCode的分布离散良好的话,那么红黑树是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,注释中给我们展示了1-8长度的具体命中概率,当长度为8的时候,概率概率仅为0.00000006,这么小的概率,HashMap的红黑树转换几乎不会发生,因为我们日常使用不会存储那么多的数据,你会存上千万个数据到HashMap中吗?

当然,这是理想的算法,但不妨某些用户使用HashMap过程导致hashCode分布离散很差的场景,这个时候再转换为红黑树就是一种很好的退让策略。

7. 为什么退化为链表的阈值是 6 ?

主要是一个过渡,避免链表和红黑树之间频繁的转换。如果阈值是7的话,删除一个元素红黑树就必须退化为链表,增加一个元素就必须树化,来回不断的转换结构无疑会降低性能,所以阈值才不设置的那么临界。

8. hash 方法
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

传入key之后,hash() 会获取key的hashCode进行无符号右移 16 位,然后进行按位异或,(即h的高16位不变,低16位与高16位按位异或)并把运算后的值返回,这个值就是key的哈希值。这样运算是为了减少碰撞冲突,因为大部分元素的hashCode在低位是相同的,不做处理的话很容易造成冲突。

// n的值是table.length
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

这其实是一种优化手段,由于数组的大小永远是一个2次幂,在扩容之后,一个元素的新索引要么是在原位置,要么就是在原位置加上扩容前的容量。这个方法的巧妙之处全在于&运算,之前提到过&运算只会关注n – 1(n =数组长度)的有效位,当扩容之后,n的有效位相比之前会多增加一位(n会变成之前的二倍,所以确保数组长度永远是2次幂很重要),然后只需要判断hash在新增的有效位的位置是0还是1就可以算出新的索引位置,如果是0,那么索引没有发生变化,如果是1,索引就为原索引加上扩容前的容量。

例:n = 16 -> 10000 n-1 -> 1111

若扩容: n-1 -> 11111

通过位运算,在每次扩容时都不用重新计算hash(可以在 HashMap 的 resize 方法中看出 newTab[e.hash & (newCap - 1)] = e;),省去了不少时间,而且新增有效位是0还是1是带有”随机性“的,之前两个碰撞的 Entry 又有可能在扩容时再次均匀地散布开,达到较好的分布离散效果

7. resize() 方法
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// initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        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;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = 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)
                    /*
                     这里就体现了 位运算 的好处,不用重新计算 hash
                     (1) 若 扩容后 hash 值增加的有效位为0,则索引值不变
                     (2) 若 扩容后 hash 值增加的有效位为1,则索引值为原来的索引值加上原来的索引容量
                     有效位 带有随机性,还可能导致之前碰撞的 Entry 在扩容后会分散开来
                     达到较好的分布离散效果
                    */

                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    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;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

二、Hashtable

特点

  • 存放的元素是键值对
  • Hashtable 的键和值都不能为 null,否则会抛出 NullPointerException
  • Hashtable 是线程安全的,而 HashMap 是线程不安全的
  • 图片描述
  • 图片描述
底层结构和源码分析
1. 构造函数
  • 底层有数组 Hashtable$Entry[] 初始化大小为 11
  • 临界值 threshold = 11 * 0.75 = 8
  • 扩容为 原来的2倍+1
public Hashtable() {
    this(110.75f);
}
    public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }
2. add() 方法
public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
}
// 添加 K-V 封装到 Hashtable$Entry
private void addEntry(int hash, K key, V value, int index) {
    modCount++;

    Entry<?,?> tab[] = table;
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}
protected void rehash() {
    int oldCapacity = table.length;
    Entry<?,?>[] oldMap = table;

    // overflow-conscious code
    //  扩容算法
    int newCapacity = (oldCapacity << 1) + 1
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        if (oldCapacity == MAX_ARRAY_SIZE)
            // Keep running with MAX_ARRAY_SIZE buckets
            return;
        newCapacity = MAX_ARRAY_SIZE;
    }
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

    modCount++;
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    table = newMap;

    for (int i = oldCapacity ; i-- > 0 ;) {
        for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
            Entry<K,V> e = old;
            old = old.next;

            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            e.next = (Entry<K,V>)newMap[index];
            newMap[index] = e;
        }
    }
}
private static class Entry<K,Vimplements Map.Entry<K,V>

三、Properties

特点:

  • Properties 类继承自 Hashtable 类,并且实现了 Map 接口
  • Properties 也是以键值对的形式保存数据,且两者均不能为 null
  • Properties 还可以用于从 xxx.properties 文件中,加载数据到 Properties 类对象,并进行读取和修改
需求1

使用 Properties 类读取配置文件(.properties 后缀)

  1. 创建 Properties 对象 (new)
  2. 加载指定配置文件 (load)
  3. 把 K-V 显式再控制台上 (list)
  4. 根据 Key 访问 Value(getProperty)
package properties;

import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;

public class TestProperties02 {
    public static void main(String[] args) throws IOException {
        // 需求:使用 Properties 类来读取 mysql.properties 配置文件

        // 1. 创建 Properties 对象
        Properties properties = new Properties();
        // 2. 加载指定配置文件
        properties.load(new FileReader("src//mysql.properties"));
        // 3. 把 K-V 显示在控制台
        properties.list(System.out);
        // 4. 根据 Key 来访问 Value
        String user = properties.getProperty("user");
        String pwd = properties.getProperty("pwd");
        System.out.println("user= " + user);
        System.out.println("pwd= " + pwd);
    }
}
需求2

使用 Properties 类创建或修改配置文件

  1. 创建 Properties 对象(new)
  2. 设置 K-V 键值对 (setProperty)
  3. 写入配置文件(store)
package properties;

import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;

public class TestProperties03 {
    public static void main(String[] args) throws IOException {
        // 需求:使用 Properties 类创建或修改 配置文件

        // 1. 创建 Properties 对象
        Properties properties = new Properties();

        // 2. setProperty 方法
        // Properties 的父类是 HashTable
        // setProperty   采用的 是 ConcurrentHashMap 的方法 (JAVA 16)
        // 如果该文件没有 Key 就是创建,若有则是修改
        properties.setProperty("pwd""88888");
        properties.setProperty("user""jianping5");
        properties.setProperty("ip""101.120.130.201");

        // 3. store 方法 将 properties 的内容写入配置文件中
        properties.store(new FileWriter("src//mysql2.properties"), null);
    }
}

具体代码可见 IDEA_projects 的 项目 chapter19 的 properties 包

四、TreeMap

特点

  • 底层是 红黑树 数据结构

  • 可以进行排序(但采用无参构造器,仍然是无序的)

  • 可以在构造器中传入一个 实现 Comparator 的匿名内部类(函数式接口/Lambda表达式)

  • 内部有个 root 是 TreeMap$Entry 类型的

Collections 的工具类

  • Collections 工具类介绍
    • Collections 是一个操作 Set、List 和 Map 等集合的工具类
    • Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作
  • 排序操作
    • reverse(List):翻转 List 中元素的顺序
    • shuffle(List):对 List 集合元素进行随机排序
    • sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
    • sott(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
    • swap(List,int,int):将指定 List 集合中的 i 处元素和 j 处元素进行交换
  • 查找、替换
    • Object max(Collection):根据元素的自然排序,返回给定集合中的最大元素
    • Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
    • Object min(Collection)
    • Object min(Collection,Comparator)
    • int frequency(Collection,Object):返回指定集合中指定元素的出现次数
    • void copy(List dest,List src):将 src 中的内容复制到 dest 中
      • 需要保证 dest 的大小 >= src 的
      • boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值

集合实现类的选择

在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择。

1. 先判断存储的类型

  • 一组对象(单列)
  • 一组键值对(双列)

2. 一组对象[单列]:Collection 接口

  • 允许重复:List
    • 增删多:LinkedList(底层维护了一个双向链表)
    • 改查多:
      • ArrayList (底层维护 Object 类型的可变数组)
      • Vector (线程安全,但效率低)
  • 不允许重复:Set
    • 无序:HashSet(底层是 HashMap,维护了一个哈希表,即(数组+链表+红黑树)
    • 排序:TreeSet(通过构造器传入排序规则)
    • 插入和取出顺序一致:LinkedHashSet,底层维护了数组+双向链表

3. 一组键值对[双列]:Map

  • 键无序:HashMap[底层是:哈希表 jdk7:数组+链表 jdk8:数组+链表+红黑树]
  • 键排序:TreeMap(通过构造器传入排序规则)
  • 键插入和取出顺序一致:LinkedHashMap
  • 读取配置文件:Properties(继承 Hashtable)

反射

反射机制允许程序在执行期间借助于 Reflection API 获得任何类的内部信息,并能操作对象的属性及方法。

加载完类后,在堆中就产生了一个 Class 类型的对象(一个类只有一个 Class 对象),通过这个对象可以得到类的结构。它就像一面镜子,通过这面镜子看到类的结构,形象地称之为反射。

缺点:反射是解释执行,速度慢。可以通过 setAccessible(true) 取消(Field、Method、Constructor)反射调用时的访问检查来优化。

1. 利用反射来操纵程序(结合配置文件)

classpath="Reflection_.Cat"
method=hi
public class TestReflection01 {
    public static void main(String[] args) throws IOException, ClassNotFoundException,
            NoSuchMethodException, InvocationTargetException,
            InstantiationException, IllegalAccessException 
{

        // 0. 利用 Properties 类 来读取配置文件
        Properties properties = new Properties();
        properties.load(new FileInputStream("src//Reflection_//re.properties"));
        String classpath = properties.get("classpath").toString();
        String methodName = properties.get("method").toString();
        System.out.println("classpath=" + classpath);
        System.out.println("methodName=" + methodName);

        // 1. 加载类 获得 Class 类型的对象 cls
        Class cls = Class.forName(classpath);
        // 2. 通过 cls 获得你加载的类的实例
        Object o = cls.getDeclaredConstructor().newInstance();
        System.out.println(o.getClass());
        // 3. 通过 cls 获得你加载的类的 方法对象
        Method method = cls.getMethod(methodName);
        // 4. 通过 method 调用你加载的类的实例的方法
        method.invoke(o);
        
        // 获得 name 域
        Field field = cls.getDeclaredField("name");
        System.out.println(field);
                
        // 获得 参数为 String 的构造器
        Constructor constructor = cls.getDeclaredConstructor(String.class);
        System.out.println(constructor);
    }
}

在不修改源码的情况下,扩容功能。(OCP 原则:开闭原则)

2. 利用反射来编写泛型数组类

package test_2;

import java.lang.reflect.Array;
import java.util.Arrays;

public class CopyOfTest {
 public static void main(String[] args) {
  int[] a = {123};
  a = (int[]) goodCopyOf(a, 10); // 获得到的新数组为 Object类型 需要强制转换
  System.out.println(Arrays.toString(a));
  System.out.println(a.getClass().getSuperclass().getName()); // 反射机制 获得其父类的类名
  
  String[] s = {"happy""sad"};
  s = (String[]) badCopyOf(s, 10); // 抛出异常 ArrayIndexOutOfBoundsException 
  System.out.println(Arrays.toString(s));  
  System.out.println(s.getClass().getSuperclass().getName()); // 反射机制 其父类 为Object
 }
 
 public static Object goodCopyOf(Object a, int newLength) {
  Class<?> c1 = a.getClass(); // 获得其Class对象
  if (!c1.isArray()) return null// 判断是否为数组
  
  Class<?> componentType = c1.getComponentType(); // 获得其类型
  int length = Array.getLength(a); // 长度
  Object newArray = Array.newInstance(componentType, newLength); // 调用Array 的newInstance 方法动态创建一个新数组
  System.arraycopy(a, 0, newArray, 0, Math.min(newLength, length)); // 将 原来数组的内容复制到新数组
  return newArray;
 }
 
 public static Object[] badCopyOf(Object[] a, int newLength) { // 允许 String[] 向上转型成 Object[]
  Object[] newArray = new Object[newLength]; // 原本是 Object[] 类型的数组 无法再强制转换成 所需要的 (String[])
  System.arraycopy(a, 0, newArray, 0, newLength);
  return newArray;
 }
}
3. 利用反射来创建对象实例

Constructor<?> constructor1 = cat.getDeclaredConstructor(int.classString.class)// 获取所有的构造器
constructors.setAccessible(true); // 暴破 则使用反射可以访问 private 构造器
Object cat1 = constructor.newInstance(5"小白"

Class 类


  1. Class 也是类,继承 Object 类
  2. Class 类对象不是 new 出来的,而是系统创建的
  3. 对于某个类的 Class 类对象,在内存中只有一份, 因为类只加载一次
  4. 每个类的实例都会记得自己是由哪个 Class 实例所生成的
  5. 通过 Class 可以完整地得到一个类的完整结构,通过一系列 API
  6. Class 对象是存放在
  7. 类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据(包括 方法代码,变量名,方法名,访问权限等)
1. 获取 Class 类对象

  1. 前提:已知一个类的全类名,且该类在类路径下,可通过 Class 类的静态方法 forName() 获取,可能抛出 ClassNotFoundException。

    实例:Class cls1 = Class.forName("java.lang.Cat");

应用场景:多用于配置文件,读取类全路径,加载类。

  1. 前提:若已知具体的类,通过 类.class 获取,该方式最为安全可靠,程序性能最高。

    实例:Class cls2 = Cat.class;

应用场景: 多用于参数传递,比如通过反射得到对应构造器对象。

  1. 前提:已知某个类的实例,调用该实例的 getClass() 方法获取 Class 对象。

​ 实例:Class cls3 = 对象.getClass(); // 运行类型

**应用场景:**通过创建好的对象,获取 Class 对象。
  1. 其他方式

​ 实例:ClassLoader cl = 对象.getClass().getClassLoader();

​ Class cls4 = cl.LoadClass("类的全类名");

  1. 基本数据(int,char,boolean,float,double,byte,long,short)按如下方式得到 Class 类对象

​ 实例:Class cls5 = 基本数据类型.class

  1. 基本数据类型对应的包装类,可以通过 .TYPE 得到 Class 类对象

     实例:Class cls6 = 包装类.TYPE
    

代理模式

1. 原理:
  • 使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理,代理对象决定是否以及何时将方法调用转到原始对象上。
2. 静态代理
  • 举例
实现 Runnable 接口的方法创建多线程
Class MyThread implements Runnable{}
Class Thread implements Runnable{}

main() {
 MyThread t = new MyThread();
 Thread thread = new Thread(t);
 thread.start();
}
  • 代理类和被代理类在编译期间,就确定下来了
  • 缺点
    • 代理类和目标对象的类都是在编译期间确定下来,不利于程序的扩展
    • 每一个代理类只能为一个接口服务,这样依赖程序开发中必然产生过多的代理
3. 动态代理
  • 动态代理是指客户通过代理类来调用其他对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象

  • 使用场合

    • 调试
    • 远程方法调用
  • 优点

    • 抽象角色中(接口)声明的所有方法都被转移到调用处理器一个集中的方法中处理,这样我们可以更加灵活的和统一的处理众多的方法。
  • 需要解决的两个主要问题

    • 如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象
      • 通过 Proxy.newProxyInstance() 实现
    • 当通过代理类的对象调用方法 a 时,如何动态的去调用被代理类中的同名方法 a
      • 通过 InvocationHandler 接口的实现类及其方法 invoke()

Java concurrent 包

Semaphore


类,控制某个资源可被同时访问的个数。

ReentranLock


类,具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。

CountDownLauch


类,可以用来在一个线程中等待多个线程完成任务的类。

Future


接口,表示异步计算的结果。

Java 并发

CopyOnWriteArrayList


CopyOnWrite :大家在共享一个内容,当有人想修改内容的时候,就创建一个改内容的副本,对副本进行修改,然后将原本的引用指向副本,完成内容的修改。(读写分离的并发策略,延时惰性策略)

优点:

  • 读取性能很高,读取的时候是无锁的(适合读多写少)
  • 读写分离策略,运行读取的时候修改集合数据。

缺点:

  • 内存占用问题
  • 数据一致性问题(只能保证最终一致性,不能保证实时一致性)

适用于 写少读多 的并发场景。

ReadWriteLock


读写锁,要求写与写之间互斥,读与写之间互斥,读与读之间可以并发执行,适用于读多写少的情况。

ConcurrentHashMap


同步的 HashMap 读写都要加锁。

注解


一、@Override

  1. 表示:该方法是个重写方法 (只能修饰方法)
  2. 若写了 @override 注解,编译器就会去检查该方法是否真的重写了父类的方法,如果的确重写了,则编译通过,否则编译错误。
  3. public @interface Override {} 表示一个注解类

二、@Deprecated

  1. 用于表示某个程序的元素(类,方法,字段,包)已经过时了(但并不代表不能用)
  2. 表示:不推荐使用,但仍然可以使用
  3. @Deprecated 可以做版本升级过渡

三、@SuppressWarnings

  1. 可以用来抑制编译器的警告
  2. @SuppressWarnings ({""}) 里面可以写入你希望抑制的警告信息 例如: all、 unused
  3. 作用范围是和你放置的位置有关

元注解


  1. Rentention 指定注解的作用范围,三种 SOURCE CLASS RUNTIME
  2. Target 指定注解可以在哪些地方使用
  3. Documented 指定该注解是否会在 javadoc 体现
  4. Inherited 子类会继承父类注解

Volatile


只能保证多线程操作的可见性,不保证原子性。

  1. 禁止了指令重排(一般 jvm 为了提高处理效率会对输入的代码进行优化,即执行顺序可能会重排)
  2. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量值,这个新值对其他线程是立即可见的
    • 当写一个 volatile 变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中
    • 这个写会操作会导致其他线程中的 volatile 变量缓存无效
  3. 不保证原子性(线程不安全)

JMM(Java内存模型)

img
img
  • 每一个线程还存在自己的工作内存,保留了被线程使用的变量的工作副本;
  • 线程对变量的所有操作都必须在工作内存中完成,而不能直接读写主内存中的变量;
  • 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。

JMM这样的规定可能导致线程对共享变量的修改没有及时更新到主内存,或者线程没能够及时将共享变量的最新值同步到工作内存中。


规范:

  • 使用 synchronized 保证原子性
  • 使用 volatile 保证可见性
  • 使用 synchronized(保证同一时刻只允许一条线程操作)或 volatile(禁止指令重排) 保证有序性。

其实JAVA已经对有序性做了些规定,Hppen-Before 原则:

  1. 程序次序原则:一个线程内,按照程序代码顺序,书写在前面的操作先发生于书写在后面的操作;
  2. volatile 规则:volatile 变量的写,先发生于读,这保证了 volatile 变量的可见性;
  3. 锁规则:解锁(unlock) 必然发生在随后的加锁(lock)前;
  4. 传递性:A先于B,B先于C,那么A必然先于C;
  5. 线程的 start 方法先于他的每一个动作;
  6. 线程的所有操作先于线程的终结;
  7. 线程的中断(interrupt())先于被中断的代;
  8. 对象的构造函数,结束先于 finalize 方法。

AOP


百科解释:

面向切面编程(Aspect Oriented Programming),通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP 是OOP 的延续,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,是函数式编程的一种衍生泛型。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

finalize方法


  1. 当对象被回收时,系统自动调用该对象的 finalize 方法。子类可以重写该方法,做一些释放资源的操作(释放数据库连接等),若不重写,则使用 Object 的 finalize 方法;

  2. 什么时候回收:当某个对象没有任何引用时,则 jvm 就认为这个对象是一个垃圾对象,就会使用垃圾回收机制来销毁该对象(不一定立马回收),在销毁该对象前,会先调用 finalize 方法;

  3. 垃圾回收机制的调用,是由系统来决定(即有自己的 GC 算法),也可以通过 System.gc() 主动触发垃圾回收机制(不一定会成功)。


https://www.nowcoder.com/test/question/done?tid=53878651&qid=14305#summary

finalize方法:这个方法一个对象只能执行一次,只能在第一次进入被回收的队列,而且对象所属于的类重写了此方法才会被执行。(若重新可达 GC Roots,则对象”复活“,延长其生命周期)。

而第二次进入回收队列的时候,不会再执行此方法,而是直接被二次标记,在下一次 GC 的时候被 GC 。

1
1

GC Roots

  • 是什么? --> JVM 可达性算法的起点

  • 特点? --> 通过它找到当前时刻存活的对象

  • 在哪里?--> 所有 java 线程当前活跃的栈帧里指向 GC 堆里的对象的引用; VM 的一些静态数据结构里指向 GC 堆里的对象的引用

Servlet


image-20220412083154017
image-20220412083154017

String 类


String x="fmn"// 1
x.toUpperCase(); // 2
String y=x.replace('f','F'); // 3
y=y+"wxy"// 4
System.out.println(y);
  1. 创建一个字符串 "fmn",在字符串常量池中,不可变;
  2. x.toUpperCase() 会对当前对象进行检查,若不需要转换则直接返回当前对象,否则 new 一个新对象返回;
  3. x.replace() 如果两个参数相同,则直接返回,否则 new 一个新对象, y 指向 “Fmn”;
  4. y = y+“wxy” 在堆中,重新 new 一个 “Fmnwxy” 对象,修改 y 指向,现在 y 指向它。

标签接口


没有定义任何方法和常量的接口

标签接口是计算机科学中的一种设计思路,用于给那些面向对象的编程语言描述对象。因为编程语言本身并不支持为类维护元数据,而标签接口可以用于描述类的元数据,弥补了这个功能上的缺失。对于实现了标签接口的类,我们就可以在运行时,通过反射机制去获取元数据。

例如:Serialiable 接口。 如果一个类实现了这个接口,则表示这个类可以被实例化,我们实际上是通过 Seraliable 接口给该类标记了 可被序列化 的元数据,打上了 可被序列化 的标签。

在 Spring 流行的时代,注解(Annotation)成为了最好的维护元数据的方式(JDK 1.5之后)。

设计模式

单例模式


饿汉式:(类一加载,对象就已经创建了,可能没有去用,若不用会造成资源浪费)

  1. 不让外部调用创建对象,所以把构造器私有化,用 private 修饰;
  2. 通过本类提供一个类方法(没有对象调用)供外部调用获取实例,用 static 修饰;
  3. 类方法只能访问调用静态变量,所以存放该实例的变量为类变量。

类变量和方法是在类加载时初始化的,只加载一次,而外部也不能创建对象。所以只有一个实例。


class Chinese{
    private static Chinese objref = new Chinese(); //2. 类的内部创建对象(static)
    private Chinese(){}; //1. 私有化 构造方法
    public static Chinese getInstance(){return objref;} 
    //3. 向外暴露一个静态的公共方法 (外界无对象引用 故 static 方法)
}
public class TestChinese{
    public static void main(String[] args){
        Chinese obj1 = new Chinese.getInstance();
        Chinese obj2 = new Chinese.getInstance();
        System.out.println(obj1 == obj2); // true
    }
}

懒汉式:(只有当用户调用 getInstance 方法时,才创建对象)

  1. 实现了单例模式,只存在一个实例对象;
  2. 只有当外界调用 getInstance 方法时,才创建对象,避免了资源浪费;
  3. 存在线程安全问题。

class Chinese{
    private static Chinese objref; //2. 声明类的内部对象(static)
    private Chinese(){}; //1. 私有化 构造方法
    public static Chinese getInstance(){  //3. 向外暴露一个静态的公共方法 (外界无对象引用 故 static 方法)
     if (objref == null) { // 实现只 创建一个对象
            objref = new Chinese("12345"); //4. 调用该方法时创建对象(存在线程安全问题)
     }
        return objref; // 返回 已经创建了的对象
   
}
public class TestChinese{
    public static void main(String[] args){
        Chinese obj1 = new Chinese.getInstance();
        Chinese obj2 = new Chinese.getInstance();
        System.out.println(obj1 == obj2); // true
    }
}

相同点:

  1. 都实现了单例模式,只存在一个实例对象

不同点:

  1. 饿汉式:类一加载,对象就被创建,可能造成资源浪费。
  2. 懒汉式:当外部调用 getInstance 方法时,对象才被创建,可能存在线程安全问题。

模板设计模式

需求


设计一个抽象类(Template),能完成以下功能:

  1. 编写方法 calculateTime(),可以计算某段代码的耗时时间
  2. 编写抽象方法 job()
  3. 编写子类 AA 和 BB ,继承抽象类 Template,并实现 job() 方法
  4. 编写一个测试类 TetstTemplate, 看是否好用

思路


  1. 执行任务的方法 放在抽象类中
  2. 任务 抽象
  3. 子类继承抽象父类,实现任务方法
  4. 子类调用父类 执行任务的方法,即可完成子类对应的任务
package Template_;

public abstract class Template // 抽象类 - 模板设计模式

    public abstract void job()// 抽象方法 由子类实现

    public void calculate() // 模板方法 可以计算子类的 任务执行时间
        long start = System.currentTimeMillis();
        job();
        long end = System.currentTimeMillis();
        System.out.println("任务执行时间 " + (end - start));
    }
}

package Template_;

public class AA extends Template{
    @Override
    public void job() {
        int nums = 0;
        for (int i = 1; i <= 100000; i++) {
            nums += i;
        }
    }
}
package Template_;

public class BB extends Template{
    @Override
    public void job() // 实现父类的抽象方法
        int nums = 0;
        for (int i = 1; i <= 700000; i++) {
            nums += i;
        }
    }
}
package Template_;

public class TestTemplate {
    public static void main(String[] args) {

        AA a = new AA();
        a.calculate();

        BB b = new BB();
        b.calculate();
    }
}

见 项目 chapter19 的 Template 包

装饰类模式


package DecoratorPattern;

public abstract class Reader_ {
    public void readFile_() {}
    public void readString_() {}
    // public void read_() {}   可以用 read_ 方法统一管理,后续利用动态绑定机制
}

package DecoratorPattern;

/**
 * 节点流(文件)
 */


public class FileReader_ extends Reader_{
    public void readFile_() {
        System.out.println("文件读取中...");
    }
}

package DecoratorPattern;

/**
 * 节点流(字符串)
 */


public class StringReader_ extends Reader_{
    public void readString_() {
        System.out.println("字符串读取中...");
    }
}

package DecoratorPattern;

/**
 * 处理流/包装流
 */


public class BufferedReader_ extends Reader_{

    // Reader_ 属性 便于将其他节点流传入进来
    private Reader_ reader_;
    public BufferedReader_(Reader_ reader_) {
        this.reader_ = reader_;
    }
    // 扩展 readFile()
    public void readFiles(int num) {
        for (int i=0; i<num; i++) {
            reader_.readFile_();
        }
    }

    // 扩展 readString()
    public void readStrings(int num) {
        for (int i=0; i<num; i++) {
            reader_.readString_();
        }
    }
}

package DecoratorPattern;

public class Test_ {
    public static void main(String[] args) {
        BufferedReader_ bufferedReader_ = new BufferedReader_(new FileReader_());
        bufferedReader_.readFiles(7);
    }
}

哈希表

百科解释:

散列表(Hash table,也叫哈希表),是根据(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称为散列函数,存放记录的数组称为散列表

哈希冲突:不同的关键值通过散列函数得到同一个下标,这就产生了哈希冲突,也叫哈希碰撞。

处理哈希冲突


1、开放寻址法

img
img

2、拉链法

img
img

哈希表的扩容


原因:当哈希表被占的位置比较多的时候,出现哈希冲突的概率也就变得很高了,所以需要进行扩容。

处理:增长因子(负载因子),已经被占的位置与总位置的一个百分比。例如:HashMap 当它的容量占总容量的百分之七十五的时候就需要扩容 ---> 新建一个数组是原来的两倍,然后把原数组的所有 Entry 都重新 Hash 一遍放到新的数组。

引用类型


  1. 强引用

Object obj = new Object() // 只要 obj 还指向 Object 对象,Object 对象就不会被回收。

只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM 也会直接抛出 OutOfMemoryError,不会去回收。若想中断强引用与对象之间的联系,可以显式的将强引用赋值为 null,这样 JVM 就可以适时的回收对象了。(GCRoots)

  1. 软引用

软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,若回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常被用来实现缓存技术(网页缓存,图片缓存等)。在 JDK1.2 之后,用 java.lang.ref.SoftReference 类来表示软引用。

  1. 弱引用

弱引用的引用强度比软引用要更弱。无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。

  1. 虚引用

虚引用是最弱的一种引用关系,若一个对象仅持有虚引用,那么它就跟没有任何引用一样,随时可能被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。唯一目的:希望能在这个对象被收集器回收时收到一个系统通知。

分类:

后端

标签:

后端

作者介绍

jianping5
V1