跳至主要內容

Java 基础

Siona大约 28 分钟

Java 基础篇

Java 语言有哪些特点?

  1. 简单易学、有丰富的类库
  2. 面向对象(Java 最重要的特性,让程序耦合度更低、内聚性更高)
  3. 平台无关性(JVM 是 Java 跨平台使用的根本)
  4. 可靠安全
  5. 支持多线程

面向对象、面向过程

  • 面向过程:是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一一调用即可。
    • 性能较高,所以单片机、嵌入式开发等一般采用面向过程开发。
  • 面向对象:是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤, 而是为了描述某个事物在解决整个问题的过程中所发生的行为。
    • 面向对象有封装、继承、多态的特性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。
    • 但是从性能上来说,比面向过程更低。

八种基本数据类型、封装类

基本类型大小(字节)默认值封装类
byte1(byte) 0Byte
short2(short) 0Short
int40Integer
long80LLong
float40.0fFloat
double80.0dDouble
boolean-falseBoolean
char2\u0000 (null)Character

注:

(1)

  • int 是基本数据类型,Integer 是 int 的封装类,是引用类型。
  • int 默认值是 0, 而 Integer 默认值是 null,所以 Integer 能区分出 0 和 null 的情况。
  • 一旦 Java 看到 null,就知道这个引用还没有指向某个对象,再任何引用使用前,必须为其指定一个对象,否则会报错。

(2)

  • 基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间,必须通过实例化开辟数据空间之后才可以赋值。
  • 数组对象也是一个引用对象,将一个数组赋值给另一个数组时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也看得见。
  • 虽然定义了 boolean 这种数据类型。

虽然定义了 boolean 这种数据类型,但是只对它提供了非常有限的支持。

在 Java 虚拟机中没有任何供 boolean 值专用的字节码指令,Java 语言表达式所操作的 boolean 值, 在编译之后都使用 Java 虚拟机中的 int 数据类型来代替,而 boolean 数组将会被编码成 Java 虚拟机的 byte 数组, 每个 boolean 元素占 8 位。这样我们可以得出 boolean 类型占了 4 个字节,在数组中又是 1 个字节。

使用 int 的原因是,对于当下 32 位的处理器(CPU)来说,一次处理数据是 32 位(这里不是指的 32 / 64 位操作系统,而是 CPU 硬件层面), 具有高效存取的特点。

标识符的命名规则

  • 标识符的含义:是指在程序中,我们自己定义的内容,譬如,类名、方法名、变量名等,都是标识符。
  • 命名规则:(硬性要求)
    • 标识符可以包含 英文字母0-9 的数字$_
    • 不能以数字开头
    • 不能是关键字
  • 命名规范:(非硬性要求)
    • 类名规范:首字母大写,后面每个单词的首字母大写(大驼峰式)
    • 变量名规范:首字母小写,后面每个单词的首字母大写(小驼峰式)
    • 方法名规范:同变量名规范

instanceof 关键字的作用

instanceof 严格来说是 Java 中的一个双目运算符,用来测试一个对象是否为一个类的实例。

boolean result = obj instanceof Class

其中 obj 是一个对象,Class 表示一个类或者一个接口, 当 obj 为 Class 的 ① 对象,或者是其 ② 直接或间接子类,或者是其 ③ 接口的实现类,结果 result 都返回 true,否则返回 false。

注意:编译器会检查 obj 是否能转换成右边的 Class 类型,如果不能转换则直接报错;如果不能确定类型,则通过编译,具体看运行时定。

int i = 0;
System.out.println(i instanceof Integer); // 编译不通过  i 必须是引用类型,不能是基本类型
System.out.println(i instanceof Object); // 编译不通过
Integer i = new Integer(1);
System.out.println(i instanceof Integer); // true
System.out.println(null instanceof Object); // false
// 在 JavaSE 规范中对 instanceof 运算符的规定是:如果 obj 为 null,则返回 false

Java 自动装箱与拆箱

  • 装箱:
    • 自动将基本数据类型转换为包装器类型(int → Integer)
    • 调用方法:Integer.valueOf(int);
  • 拆箱:
    • 自动将包装器类型转换为基本数据类型(Integer → int)
    • 调用方法:Integer.intValue();

在 Java SE5 之前,如果要生成一个数值为 10 的 Integer 对象,必须这样实现:

Integer i = new Integer(10);

从 Java SE5 开始,提供了自动装箱的特性,如果要生成一个数值为 10 的 Integer 对象,直接用以下方式可以实现:

Integer i = 10;

面试题 1️⃣ 以下代码会输出什么?

public class Main {
    public static void main(String[] args) {
        Integer i1 = 100;
        Integer i2 = 100;
        Integer i3 = 200;
        Integer i4 = 200;
        
        System.out.println(i1 == i2);
        System.out.println(i3 == i4);
    }
}

运行结果:

true
false

为什么会出现这样的结果?输出结果表明 i1 和 i2 指向的是同一个对象,而 i3 和 i4 指向的是不同的对象。 此时只需一看源码便知究竟,以下是 Integer 的 valueOf 方法的具体实现:

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

其中 IntegerCache 类的实现为:

private static class IntegerCache {
    static final int high;
    static final Integer cache[];
    
    static {
        final int low = -128;
        
        // high value may be configured by property
        int h = 127;
        if (integerCacheHighPropValue != null) {
            // Use Long.decode here to avoid invoking methods that
            // require Integer's autoboxing cache to be initialized
            int i = Long.decode(integerCacheHighPropValue).intValue();
            i = Math.max(i, 127);
            // Maximum array size is Integer.MAX_VALUE
            h = Math.min(i, Integer.MAX_VALUE - -low);
        }
        high = h;
        
        cache = new Integer[(high - low) + 1];
        int j = low;
        for (int k = 0; k < cache.length; k++) {
            cache[k] = new Integer(j++);
        }
    }
    
    private IntegerCache() {}
}

从这两段代码可以看出,在通过 valueOf 方法创建 Integer 对象的时候, 如果数值在 [-128, 127] 之间,便返回指向 IntegerCache.cache 中已经存在的对象的引用; 否则创建一个新的 Integer 对象。

上面的代码中 i1 和 i2 数值为 100,因此会直接从 cache 中取已经存在的对象,所以 i1 和 i2 指向的是同一个对象, 而 i3 和 i4 则分别指向不同的对象。

面试题 2️⃣ 以下代码输出什么?

public class Main {
    public static void main(String[] args) {
        Double i1 = 100.0;
        Double i2 = 100.0;
        Double i3 = 200.0;
        Double i4 = 200.0;
        
        System.out.println(i1 == i2);
        System.out.println(i3 == i4);
    }
}

运行结果:

false
false

原因:在某个范围内的整型数值的个数是有限的,而浮点数不是。

重写、重载

重写(Override)

  1. 从字面上看,重写就是 “重新写一遍” 的意思。其实就是在子类中把父类本身有的方法重新写一遍。

  2. 子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法, 所以在方法名、参数列表、返回类型(如果子类中方法的返回值是父类方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写。

  3. 注意:子类函数的访问修饰权限不能少于父类的。

public class Father {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Son s = new Son();
        s.sayHello();
    }
    
    public void sayHello() {
        System.out.println("Hello");
    }
}

class Son extends Father {
    @Override
    public void sayHello() {
        // TODO Auto-generated method stub
        System.out.println("hello by ");
    }
}
  • 发生在父类与子类之间
  • 方法名、参数列表、返回类型(如果子类中方法的返回类型是父类中返回类型的子类)必须相同
  • 访问修饰符的限制一定要大于被重写方法的访问修饰符(public > protected > default > private)
  • 重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常

重载(Overload)

  • 重载发生在同一个类中
  • 在一个类中,方法名相同、参数列表不同(参数类型不同、参数个数不同、参数顺序不同) → 方法重载
  • 重载对返回类型没有要求,可以相同也可以不同,所以不能通过返回类型是否相同来判断重载。
public class Father {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Son s = new Son();
        s.sayHello();
        s.sayHello("Siona");
    }
    
    public void sayHello() {
        System.out.println("Hello");
    }
    
    public void sayHello(String name) {
        System.out.println("Hello " + name);
    }
}
  • 重载 Overload 是一个类中多态性的一种表现
  • 重载要求同名方法的参数列表不同(参数类型,参数个数、参数顺序)
  • 重载时,返回值类型可以相同、也可以不同。返回值类型不能作为重载函数的区分标准。

总结

  • 重载
    • 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
  • 重写
    • 发生在父子类中,方法名、参数列表必须相同,返回值返回小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;
    • 如果父类方法访问修饰符为 private,则子类就不能重写该方法。
public int add(int a, String b)
public String add(int a , String b)  // 编译报错

equals==

==

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

  • 比较的是操作符两端的操作数是否是同一个对象
  • 两边的操作数必须是同一类型(可以是父子类之间)才能编译通过
  • 比较的是地址,如果是具体的阿拉伯数字的比较,值相等则为 true。如,int a = 10long b = 10Ldouble c = 10.0 都是相同的(true),因为它们都指向地址为 10 的堆。

equals

equals 比较两个对象的内容是否相等,由于所有的类都是继承自 java.lang.Object 类,所以适用于所有对象。

如果没有对该方法进行覆盖的话,调用的仍然是 Object 类中的方法,而 Object 类中的 equals() 返回的是 == 的判断。

总结

  • 比较是否相等,全部用 equals()
  • 与常量进行比较时,把常量写在前面,因为使用 Object 的 equals(),Object 可能为 null,报空指针。"值".equals(obj)Object.equals("值", name)
  • 阿里代码规范中,只使用 equals(),阿里插件默认会识别,并可以快速修改,推荐安装阿里插件来排查老代码使用 == ,替换成 equals()

图灵的解释:

  • == 对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址。
  • equals Object 中默认采用 == 比较,通常会重写。
// Object.java 类
public boolean equals(Object obj) {
    return (this == obj);
}
// String.java 类
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- = 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

上述代码可以看出,String 类中被复写的 equals() 方法其实是比较两个字符串的内容

HashCode 的作用

Java 的集合类:

  • List
    • 有序、可重复
  • Set
    • 无序、不可重复

使用场景:

Set 中插入元素时,如何判断是否已经存在该元素?

方案1:通过 equals()。但是如果元素太多,该方法就会比较慢。

进阶2:有人发明了 哈希算法 来提高集合中查找元素的效率。这种方法将集合分成若干个存储区域,每个对象可以计算出一个 哈希码,可以将哈希码分组, 每组分别对应某个存储区域,根据一个对象哈希码就可以确定该对象应该存储在哪个区域。

  • hashCode() 理解:它返回的是根据对象的内存地址换算出的一个值。
  • ① 当集合要添加新的元素时,先调用这个元素的 hashCode(),然后定位到它应该放置的物理位置上。
  • ② 如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;
  • ③ 如果这个位置上已经有元素了,就调用它的 equals() 与新元素进行比较,相同的话就跳过,不相同就散列其它地址。
  • 这样,实际调用 equals() 的次数大大降低,几乎只需要一两次。

String、StringBuffer、StringBuilder

  • String
    • 只读字符串,String 不是基本数据类型,而是一个对象。
    • 从底层源码来看,String 是一个 final 类型的字符数组,所引用的字符串不能被改变,一经定义,无法再增删改。
    • 每次对 String 的操作都会生成一个新的 String 对象。
/* String 底层源码 */
private final char value[]; 

每次 + 操作:隐式地在堆上 new 了一个原字符串相同的 StringBuilder 对象,再调用 append 方法,拼接 + 后面的字符。

  • StringBuffer
    • 继承 AbstractStringBuilder 抽象类
    • 底层是可变的字符数组
    • 适合场景:频繁地字符串操作
    • StringBuffer 对方法加了同步锁 或者 对调用的方法加了同步锁,所以是 线程安全 的。
  • StringBuilder
    • 继承 AbstractStringBuilder 抽象类
    • 底层是可变的字符数组
    • 适合场景:频繁地字符串操作
    • StringBuilder 没有对方法加同步锁,所以是 非线程安全 的。
/**
* AbstractStringBuilder.java 抽象类
* The value is used for character storage.
*/
char[] value;

总结

  • Stringfinal 修饰的,不可变,每次操作都会产生新的 String 对象。
  • StringBufferStringBuilder 都是在原对象上操作。
  • StringBuffer 是线程安全的,StringBuilder 线程不安全。
  • StringBuffer 方法都是 synchronized 修饰的
  • 性能:StringBuilder > StringBuffer > String
  • 场景:
    • 经常需要改变字符串内容时,使用 StringBufferStringBuilder
    • 优先使用 StringBuilder,多线程使用共享变量时,使用 StringBuffer

List、Set

  • List
    • 有序,按对象进入的顺序保存对象
    • 可重复,允许多个 null 元素对象
    • 可以使用 iterator 取出所有元素,再逐一遍历,还可以使用 get(int index) 获取指定下标的元素
  • Set
    • 无序,不可重复,最多只有一个 null 元素对象
    • 取元素时,只能用 iterator 接口取得所有元素,再逐一遍历

ArrayList、LinkedList

  • Array(数组)
    • Array 是基于索引(index)的数据结构。
    • 它使用索引在数组中的搜索和读取数据是非常快的。
    • 查找:时间复杂度 O(1)
    • 删除:开销很大,需重排数组中所有元素(前移)
    • 缺点:数组初始化必须执行初始化的长度,否则报错
int[] a = new int[4];  // 推荐使用 int[] 方式初始化

int c[] = {1, 2, 3, 4};  // 长度:4,索引范围:[0,3]
  • List(集合)
    • 有序集合,元素可重复,提供了按索引访问的方式,继承 Collection 类。
    • List 有两个重要的实现类:ArrayList、LinkedList。
    • ArrayList
      • 能够自动增长容量的数组
      • ArrayList.toArray() 返回一个数组
      • ArrayList.asList() 返回一个列表
      • ArrayList 底层是 Array,数组扩容实现
    • LinkedList
      • 双链表
      • LinkedList 在添加、删除元素时,比 ArrayList 性能高。【数据量很大、频繁操作时】
      • LinkedList 在 get、set 元素,比 ArrayList 性能差。【数据量很大、频繁操作时】

总结

  • ArrayList
    • 基于 动态数组,连续内存存储,适合下标访问(随机访问)
    • 扩容机制:因为数组长度固定,超出长度存数据时,需要新建数组,然后将老数组的数据拷贝到新数组。
    • 如果不是尾部插入数据元素还是涉及到元素的移动(往后复制一份,插入新元素)。
    • 使用 尾插法 并指定初始容量,可以极大提升性能、甚至超过 LinkedList(需要创建大量的 node 对象)。
  • LinkedList
    • 基于 链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询(需要逐一遍历)。
    • 遍历:必须使用 iterator,不能使用 for 循环。因为每次 for 循环体内通过 get(i) 取得某一元素时都需要对 list 重新进行遍历,性能消耗极大。
    • 不要试图使用 indexOf 等返回元素索引,并利用其进行遍历,使用 indexOf 对 list 进行遍历,当结果为空时会遍历整个 list。

HashMap、HashTable

(1)两者父类不同

  • HashMap 继承自 AbstractMap 类。
  • HashTable 继承自 Dictionary 类。
  • 🪣 两者都实现了 Map、Cloneable(可复制)、Serializable(可序列化)三个接口。

(2)对外提供的接口不同

  • HashTable 比 HashMap 多提供了 elements()contains() 两个方法。
  • elements()
    • 该方法继承自 HashTable 的父类 Dictionary。
    • 用于返回此 HashTable 中的 value 的枚举。
  • contains()
    • 作用:判断该 HashTable 是否包含传入的 value。同 containsValue() 一致。
    • 事实上,containsValue() 只是调用了一下 contains() 方法。

(3)对 null 的支持不同

  • HashTable
    • key 和 value 都不可为 null
  • HashMap
    • key 可以为 null,但只有一个,因为 key 必须保证唯一性;【key 只有一个 null
    • 可以有多个 key 对应的 value 为 null。【value 可有多个 null

(4)安全性不同

  • HashMap

    • 线程不安全
    • 在多线程并发环境下,可能会产生 死锁 等问题,因此需要开发人员自己处理多线程的安全问题。
  • HashTable

    • 线程安全
    • HashTable 每个方法上都有 synchronized 关键字,因为可直接用于多线程中。
  • 💡虽然 HashMap 线程不安全,但它的效率远远高于 HashTable,因为大部分的使用场景是 单线程

  • ❗️ 当需要多线程操作时,可以使用 线程安全ConcurrentHashMap

  • ConcurrentHashMap

    • 线程安全
    • 效率比 HashTable 高好多倍。
    • 因为 ConcurrentHashMap 使用了 分段锁,并不对整个数据进行锁定。

(5)初始容量大小和每次扩容量大小不同

(6)计算 hash 值的方法不同

总结

  • HashMap
    • HashMap 方法没有 sychronized 修饰,线程不安全
    • 允许 keyvaluenull
    • 底层实现:数组+链表
      • JDK8 开始链表高度为 8、数组长度超过 64,链表转变为 红黑树,元素以 内部类 Node 节点 存在
      • 计算 key 的 hash 值,二次 hash 然后对数组长度取模,对应到数组下标。
      • 如果没有产生 hash 冲突(下标位置没有元素),则直接创建 node 存入数组;
      • 如果产生 hash 冲突,先进行 equals 比较。相同,则取代该元素;不同,则判断链表高度插入链表,链表高度达到 8,并且数组长度到 64 则转变为红黑树,长度低于 64 则将红黑树转回链表。
      • key 为 null 时,存储在下标 0 的位置。
    • 数组扩容:
  • HashTable
    • 线程安全
    • 不允许 keyvaluenull
    • 底层实现:
    • 数组扩容:

ConcurrentHashMap 原理

JDK 7

  • 数据结构:
    • ReentrantLock + Segment + HashEntry
    • 一个 Segment 中包含一个 HashEntry 数组,每个 HashEntry 又是一个链表结构。
  • 元素查询:
    • 二次 hash。第一次 hash 定位到 Segment,第二次 hash 定位到元素所在链表的头部。
  • 锁:Segment 分段锁
    • Segment 继承了 ReentrantLock,锁定操作的 Segment,其他的 Segment 不受影响,并发度为 Segment 个数,可以通过构造函数指定,数组扩容不会影响其他的 Segment。
    • get 方法无需加锁,volatile 保证可见性

JDK 8

  • 数据结构:
    • synchronized + CAS + Node + 红黑树
    • Node 的 val 和 next 都用 volatile 修饰,保证可见性。
    • 查找、替换、赋值操作都使用 CAS。
  • 锁:锁链表的 head 节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容。
  • 读操作无锁:
    • Node 的 val 和 next 使用 volatile 修饰,读写线程对该变量互相可见。
    • 数组用 volatile 修饰,保证扩容时被读线程感知。

Collection 包结构、Collections

  • Collection
    • 集合类的上级接口。
    • 子接口有:Set、List、LinkedList、ArrayList、Vector、Stack;
  • Collections
    • 集合类的一个帮助类。
    • 它包含各种有关集合操作的静态、多态方法,用于实现对各种集合的搜索、排序、线程安全化等操作。
    • 该类不能实例化,就像一个工具类,服务于 Java 的 Collection 框架。

Java 的四种引用,强弱软虚

(1)强引用

  • 强引用是平时使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收。
String str = new String("str");
System.out.println(str);

(2)软引用

  • 软引用,在程序内存不足时,会被回收。
  • 可用场景:创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM 就会回收早先创建的对象。
SoftReference<String> srf = new SoftReference<String>(new String("str"));
// 注意:srf 是强引用,它指向 SoftReference 对象,
// 这里的软引用:指向 new String("str") 的引用,也就是 SoftReference 类中 T

(3)弱引用

  • 弱引用,只要 JVM 垃圾回收器 发现了它,就会将之回收。
  • 可用场景:Java 源码中的 java.util.WeakHashMap 中的 key 就是弱引用。
  • 简单理解:一旦不需要某个引用,JVM 会自动将之回收,不需要我们做其它操作。
WeakReference<String> wrf = new WeakReference<String>(str);

(4)虚引用

  • 在被回收之前,会被放入 ReferenceQueue 中。
  • 📢 其它引用是被 JVM 回收之后,才被放入 ReferenceQueue 中。
  • 虚引用,大多被用于 引用销毁前的处理工作
  • 虚引用创建时,必须带有 ReferenceQueue
  • 可用场景:对象销毁前的一些操作,比如资源释放等。Object.finalilze() 虽然也可以做此类动作,但此方式不安全、低效。
PhantomReference<String> prf = new PhantomReference<String>(new String("str"), new ReferenceQueue<>());

泛型

泛型,JavaSE 1.5 之后的特性,《Java 核心技术》中对泛型的定义是:

“泛型” 意味着编写的代码可以被不同类型的对象所重用。

  • “泛型”,顾名思义,“泛指的类型”。
  • 虽然有了泛型的概念,但具体执行的时候,却可以有具体的规则来约束。
  • 比如,ArrayList 就是泛型类,ArrayList 作为集合可以存放各种元素(Integer、String、自定义的各种类型等)。
  • 但是,在使用的时候,通过具体的规则来约束。
// 约束集合中只存放 Integer 类型的元素
List<Integer> list = new ArrayList<>();

Java 创建对象的 4 种方式

  • new 创建新对象
  • 通过反射机制
  • 采用 clone 机制
  • 通过序列化机制

Hash 冲突

HashCode 相同的场景【Hash 冲突】 面试题 1️⃣ 两个不相等的对象,可能有相同的 hashCode❓

答:有可能。在产生 Hash 冲突时

处理 Hash 冲突的方式:

  • 拉链法:每个 hash 表的节点都有一个 next 指针,多个 hash 表节点可以用 next 指针构成一个单向链表,被分配到同一个索引上的多个节点可以用该单向链表进行存储。
  • 开放地址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
  • 再哈希法:又叫双哈希法,有多个不同的 hash 函数。当发生冲突时,使用第二个、第三个、…… 等哈希函数计算地址,直到无冲突。

深拷贝、浅拷贝

  • 深拷贝
    • 被复制对象的所有变量都含有与原来的对象相同的值。而那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。
    • 换言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
  • 浅拷贝
    • 被复制对象的所有变量都含有与原来的对象相同的值。而所有的对其他对象的引用仍然指向原对象。
    • 换言之,浅拷贝仅仅复制了该对象,并没有复制它索引用的对象。

final 的用法

  • 被 final 修饰的类,不可以被继承
  • 被 final 修饰的方法,不可以被重写
  • 被 final 修饰的变量,不可以被改变。如果修饰引用,则表示引用不可变,引用指向的内容可变。
  • 被 final 修饰的方法,JVM 会尝试将其内联,以提高运行效率。
  • 被 final 修饰的常量,在编译阶段会存入常量池中

编译器对 final 域要遵守的两个重排规则:

  • 在构造函数内对一个 final 域的写入,与随后把

接口、抽象类

  • 抽象类可以存在普通成员函数,而接口中只能存在 public abstract 方法。
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
  • 抽象类只能继承一个,接口可以实现多个。

设计目的

  • 接口:对类的行为进行 “约束”。
    • 更准确的说是一种 “有” 约束,因为接口不能规定类不可以有什么行为。
    • 也就是提供一种机制,可以强制要求不同的类具有相同的行为。
    • 它只约束了行为的有无,但不对如何实现行为进行限制。
  • 抽象类:代码复用。
    • 当不同的类具有某些相同的行为(记为行为集合 A),且其中一部分行为的实现方式一致时(A 的非真子集,记为 B),可以让这些类都派生于一个抽象类。 在这个抽象类中实现了 B,避免让所有的子类来实现 B,这就达到了代码复用的目的。
    • A - B 的部分,留给各个子类自己实现。
    • 正是因为 A-B 在这里没有实现,所以抽象类不允许实例化出来。(否则当调用到 A-B 时,无法执行)

英文助记

  • 抽象类:对类本质的抽象。
    • 表达的是 is a 的关系,比如:BMW is Car
    • 抽象类包含并实现了子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。
  • 接口:对行为的抽象。
    • 表达是的 like a 的关系。比如:Bird like a Aircraft。(像飞行器一样可以飞),但其本质上 is a Bird
    • 接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。

使用场景

  • 抽象类:关注一个事物的本质时,用抽象类;
  • 接口:关注一个操作时,用接口。

区别

  • 抽象类的功能要 远超于 接口,但是抽象类的 代价高
  • 因为从高级语言来说(从实际设计上来说),每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。
  • 虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口。在设计阶段会降低难度。

Java 异常

  • Throwable:顶级父类。下有两个子类:ExceptionError
    • Exception:不会导致程序停止。
      • RuntimeException:运行时异常。发生在程序运行过程中,会导致程序当前线程执行失败。
      • CheckedException:检查时异常。发生在程序编译过程从,会导致程序编译不通过。
    • Error:程序无法处理的错误,一旦出现这个错误,程序将被迫停止运行。

Java 类加载器

JDK 自带 3 个类加载器:

BootStrapClassLoader

BootStrapClassLoader 是 ExtClassLoader 的父类加载器,默认负责加载 %JAVA_HOME%/lib 下的 jar 包和 class 文件。

ExtClassLoader

ExtClassLoader 是 AppClassLoader 的父类加载器,负责加载 %JAVA_HOME%/lib/ext 文件夹下的 jar 包和 class 文件。

AppClassLoader

AppClassLoader 是自定义加载器的父类,负责加载 classpath 下的类。其称为系统类加载器、线程上下文加载器。

自定义加载器

继承 ClassLoader 实现自定义类加载器。

双亲委派模型

双亲委派模型.png
双亲委派模型.png

好处

  • 安全性:避免用户自己编写的类动态替换 Java 的一些核心类,比如 String。
  • 避免了类的重复加载,因为 JVM 中区分不同类,不仅仅是根据类名,相同的 class 文件被不同的 ClassLoader 加载就是不同的两个类。

线程、并发

线程的生命周期、状态

线程的 5 种状态: 创建、就绪、运行、阻塞、死亡。

阻塞的 3 种情况:

阻塞类型描述
等待阻塞运行的线程执行 wait 方法,该线程会释放占用的所有资源,JVM 会将该线程放入 “等待池” 中。
进入这个状态之后,是不能自动唤醒的,必须依靠其他线程调用 notifynotifyAll 方法才能被唤醒。
wait 是 Object 类的方法。
同步阻塞运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会将该线程放入 “锁池” 中。
其他阻塞运行的线程执行 sleepjoin 方法,或者发出了 I/O 请求 时,JVM 会将该线程置为阻塞状态。
当 sleep 状态超时、join 等待线程终止或超时、I/O 处理完毕时,线程重新转入就绪状态。
sleep 是 Thread 类的方法。
状态描述
新建状态(New)新创建了一个线程对象。
就绪状态(Runnable)线程对象创建后,其他线程调用了该对象的 start 方法。
该状态的线程位于 可运行线程池 中,变得可运行,等待获得 CPU 的使用权。
运行状态(Running)就绪状态的线程获取了 CPU,执行程序代码。
阻塞状态(Blocked)阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
死亡状态(Dead)线程执行完了或者因异常退出了 run 方法,该线程结束生命周期。

sleep()、wait()、join()、yield() 区别

锁池

所有需要 竞争同步锁的线程 都会放在锁池当中, 比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待; 当前面的线程释放同步锁后,锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待 CPU 资源分配

等待池

当我们调用 wait() 方法后,线程会放到等待池中,等待池的线程不会去竞争同步锁。 只有调用了 notify()notifyAll() 后等待池的线程才会开始去竞争同步锁。 notify() 是随机从等待池选出一个线程放到锁池,而 notifyAll() 是将等待池的所有线程放到锁池中。

sleep() 和 wait()

  1. sleep() 是 Thread 类的静态本地方法;wait() 是 Object 类的本地方法。
  2. sleep() 不会释放 lockwait() 会释放 lock,并加入到等待队列中。
  3. sleep() 不依赖同步器 synchronizedwait() 需要依赖 synchronized 关键字。
  4. sleep() 不需要被唤醒(休眠之后退出阻塞);wait() 需要被唤醒(不指定时间需要被别人中断)。
  5. sleep() 一般用于当前线程休眠,或者轮询暂停操作;wait() 则多用于多线程直接的通信。
  6. sleep() 会让出 CPU 执行时间且强制上下文切换;wait() 则不一定,wait 后可能还是有机会重新竞争到锁继续执行的。

yield()

yield() 执行后线程进入就绪状态,马上释放了 CPU 的执行权,但是依然保留了 CPU 的执行资格, 所以有可能 CPU 下次进行线程调度时,还会让这个线程获取到执行权继续执行。

join()

join() 执行后线程进入阻塞状态。例如,在线程 B 中调用线程 A 的 join(),则线程 B 会进入到阻塞队列,直到线程 A 结束或中断。

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("222222");
        }
    });
    t1.start();
    t1.join();
    // 这行代码必须要等 t1 全部执行完毕,才会执行
    System.out.println("111");
}

// 执行结果
222222
111

线程安全

不是线程安全,应该是内存安全,堆是共享内存,可以被所有线程访问。

定义

当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的。

堆是进程和线程共有的空间,分全局堆、局部堆。

  • 全局堆:所有没有分配的空间。
  • 局部堆:用户分配的空间。

堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。

在 Java 中,堆是 Java 虚拟机管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。 堆所存在的内存区域的唯一目的就是 存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

  • 栈是每个线程独有的,保存其运行状态和局部变量。
  • 栈在线程开始的时候初始化,每个线程的栈相互独立。因此,栈是线程安全的。
  • 操作系统在切换线程的时候会自动切换栈。
  • 栈空间不需要在高级语言里,显式的分配和释放。