代码块

创建一个对象时,在一个类的调用顺序时:

  1. 调用静态代码块和静态属性初始化(注意:静态代码块和静态属性初始化调用的优先级一样,如果有多个静态代码块和多个静态变量初始化,则按他们定义的顺序调用
  2. 调用普通代码块和普通属性的初始化
  3. 调用构造方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class codeBlock {
public static void main(String[] args) {
A a = new A(); /**/
}
}
class A {
private int n2 = getN2();
{
System.out.println("A 的普通代码块");
}
static {
System.out.println("A 静态代码块");
}

private static int n1 = getN1();

public static int getN1() {
System.out.println("getN1被调用...");
return 100;
}
public int getN2() {
System.out.println("getN2被调用...");
return 200;
}

public A() {
System.out.println("A() 被调用");
}
}

执行结果:

A 静态代码块
getN1被调用…
getN2被调用…
A 的普通代码块
A() 被调用

如果有继承关系,执行的顺序为:父类静态属性 –> 子类静态属性 –> 父类普通属性 –> 子类普通属性 –> 父类构造器 –> 子类构造器

静态代码块只能调用静态成员,普通代码块可以使用任意成员

final static一起使用不会进行类加载(代码块也不会调用):public final statuc int num = 1000;

接口

接口就是给出一些没有实现的方法,封装到一起没到某个类要使用的时候,再根据具体情况把这些方法写出来。如果一个类实现(implements)接口,则需要将该接口的所有抽象方法都实现。

在 jdk8 后,可以有默认实现方法,需要使用default关键字修饰,也可以有静态方法

接口中的所有方法是 public 方法,接口中抽象方法,可以不用 abstract修饰

抽象类去实现接口时,可以不是实现接口的抽象方法

接口中的属性,只能是final而且是public static final修饰符。比如:int a = 1,实际上是public static final int a = 1

小结

当子类继承了父类,就自动拥有父类的功能,如果子类需要扩展功能可以通过实现接口的方式扩展,可以理解实现接口时对Java但继承机制的一种补充。

继承的价值主要在于:解决代码的复用性和可维护性

接口的价值主要在于:设计,设计好各种规范(方法),让其它类去实现这些方法,让其更加灵活

内部类

一个类的内部有完整的嵌套了另一个类结构,被嵌套的类就称为内部类(inner class),嵌套其他类的类称为外部类(outer class)

局部内部类

局部内部类是定义在外部类的局部位置,通常在方法中。局部内部类可以直接访问外部类的所有成员

1
2
3
4
5
6
7
8
9
10
11
class Outer {
private int n1 = 100;
private void m2() {} // 私有方法
public void m1() {
class Inner { // 局部内部类
public void f1() {
System.out.println("n1 = " + n1);
}
}
}
}

局部内部类不能添加访问修饰符,但是可以用final修饰,作用域:仅仅在定义它的方法或代码块中。

如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,需要使用外部类名.this.成员去访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Inner {
public static void main(String[] args) {
Outer outer = new Outer();
outer.m1(); // InnerClass n1 = 800
// OuterClass n1 = 100

}
}

class Outer {
private int n1 = 100;
private void m2() {} // 私有方法
public void m1() {
class Inner { // 局部内部类
private int n1 = 800;
public void f1() {
System.out.println("InnerClass n1 = " + n1);
System.out.println("OuterClass n1 = " + Outer.this.n1);
}
}
Inner inner = new Inner();
inner.f1();
}
}

匿名内部类

匿名内部类是定义在外部类的局部位置,它本质还是一个类,但该类没有名字,而且它同时还是一个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Outer04 {
private int n1 = 10;

public void method() {
// 基于接口的匿名内部类
IA tiger = new IA() {
@Override
public void cry() {
System.out.println("老虎呼唤...");
}

public void func() {
System.out.println("特有的方法");
}
};
tiger.cry();
System.out.println(tiger.getClass());
}
}

interface IA {
public void cry();
}

tiger的编译类型为 IA运行类型就是匿名内部类。

原理:jdk 底层会分配类名Outer04$1,在创建内部类 Outer04$1后立刻就创建了Outer04$1实例,并把地址返回给 tiger

成员内部类

其他类中创建成员内部类

1
2
3
4
5
6
7
8
class Outer01 {
private int n1 = 100;
public class Inner01 {
public void hi() {
System.out.println("hi...");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
public class InnerClassExercise02 {
public static void main(String[] args) {
// 第一种
Outer01 outer01 = new Outer01();
Outer01.Inner01 inner01 = outer01.new Inner01();
inner01.hi();
// 第二种
Outer01.Inner01 inner011 = new Outer01().new Inner01();
inner011.hi();
}
}

enum

常用方法

image-20211208213101914

异常类

异常体系图

image-20211209132502605

try-catch-finally

1
2
3
4
5
6
7
8
9
10
try {
// 可能有异常,一旦有异常直接进入 catch 中
}catch(Exception e) {
// 捕获到异常
// 当异常发生时系统将异常封装成 Exception 对象 e,传递给 catch
// 得到异常对象后,程序员自己处理
// 注意:如果没有发生异常,catch 代码块将不会执行
}finally {
// 不管 try 代发快是否发生异常,始终要执行 finally,所以,通常将释放资源的代码 放在 finally
}

自定义异常类

1
2
3
4
5
class Custom extends RuntimeException{
public Custom(String message) {
super(message);
}
}

throw 和 throws 的区别

意义 位置 后面跟的东西
throws 异常处理的一种方式 方法声明处 异常类型
throw 手动生成异常对象的关键字 方法体中 异常对象

String 类

继承图

image-20211209213739028

String 类实现了接口 Serializable 【String 可以串行化(序列化):可以在网络传输)

String 类实现了接口 Comparable 【String 对象可以比较大小】

String 是 final 类,不能被继承

**String 有属性 private final char value[];用于存放字符串内容,不可修改(对象不能修改)**,value不能指向新的地址,但是单个字符的内容是可以改变的

1
2
3
4
5
final char[] value = {'a', 'b', 'c'};
value[0] = 'b'; // 可行

char[] s = {'q', 'q', 'q'};
value = s; // error

两种创建String对象的区别

方式一:直接赋值 String s = "hso";

方式二:调用构造器 String s2 = new String("hso");

方式一:先从常量池查看是否有"hso"数据空间,如果有,直接指向;如果没有则重新创建,然后指向。s 最终指向的是常量池的空间地址

方式二:先从堆中创建空间,里面维护了value属性,指向常量池的"hso"空间。如果常量池没有"hso",则重新创建,如果有,直接通过value指向,**最终指向的是堆中的空间地址**

image-20211209221436474

intern()

intern() 方法最终返回的是常量池的地址

1
2
3
4
5
String a = "hso";
String b = new String("hso");
System.out.println(a == b); // false
System.out.println(a == b.intern()); // true
System.out.println(b == b.intern()); // false

例题一

1
2
3
4
5
6
String a = "hello";
String b = "abc";
String c = a + b;
String d = "helloabc";
System.out.println(d == c.intern()); // true
System.out.println(d == c); // false

image-20211209230314419

重要规则String c1 = "ab" + "cd";常量相加,看的是常量池。String c1 = a + b;变量相加,是在堆中。

底层为:StringBuilder sb = new StringBuilder(); sb.append(a); sb.append(b); sb实在堆中,并且 append是在原来字符串的基础上追加的。

例题二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class String03 {
public static void main(String[] args) {
Test ex = new Test();
ex.change(ex.str, ex.ch);
System.out.print(ex.str + " and ");
System.out.println(ex.ch);
}
}

class Test {
String str = new String("hsp");
final char[] ch = {'j','a','v','a'};
public void change(String str, char[] ch) {
str = "java";
ch[0] = 'a';
}
}

输出为:hsp and aava

还没调用change函数时的内存分布图

image-20211211090511733

调用ex.change(ex.str, ex.ch)后会在栈中开辟新的空间,形参str指向堆中的value,形参ch指向java的那片空间,调用change函数后形参str就不指向value直接指向常量池中的"java"。形参ch调用后直接改变java的值。

image-20211211090546722

也就是说ex对象指向的地方并没有改变所以最后ex.str输出的还是"hsp"

有这道题衍生出,如果想要改变ex.str的值要怎么办呢?由内存图可得,让str指向常量池中的"java"即可。在类Test加一个change2方法

1
2
3
4
5
public void change2(Test ex)
{
ex.str = "java";
ex.ch[0] = 'h';
}
1
2
3
4
5
6
7
8
9
public class String03 {
public static void main(String[] args) {
Test ex = new Test();
// ex.change(ex.str, ex.ch);
ex.change2(ex);
System.out.print(ex.str + " and ");
System.out.println(ex.ch);
}
}

因为这是引用传值,所以mainex也会跟着改变,最后内存分布图为:

image-20211211101628251

结果如下:

image-20211211101246082

String 、StringBuffer 、StringBuilder

String:不可变字符序列,效率低,但是复用率高

StringBuffer:可变字符序列,效率较高(增删)、线程安全

StringBuilder:可变字符序列,效率最高,线程不安全,适合在单线程使用

String使用注意说明

String s = "a"; //创建了一个字符串

s += "b"; // 实际上原来的 “a” 字符串对象已经丢弃了,现在又产生一个字符串 s + “b”(也就是 “ab”)。如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大地影响程序的性能

结论:如果我们对 String 做大量修改,不要使用 String

集合

集合体系框架图

  1. 单列集合

    image-20211212141632697

  2. 双列集合

    image-20211212141608685

ArrayList

  1. ArrayList 中维护了一个 Object 类型的数组 elementDatatransient Object[] elementData;
    transient表示瞬间 短暂的,表示该属性不会被序列化

  2. 当创建 ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为 0,第一次添加,则扩容10,如果需要再次扩容,则扩容elementData到 1.5 倍。如(10 –> 15 –> 22…)

  3. 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData到 1.5 倍。如(8 –> 12 –>18…)

无参构造器

1
2
3
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

创建了一个空的elementData数组

有参构造器

1
2
3
4
5
6
7
8
9
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);
}
}

底层扩容机制

通过add方法扩容

1
2
3
4
5
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
  1. 先确定是否要扩容
  2. 然后再执行赋值操作
1
2
3
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

这个方法主要来执行calculateCapacityensureExplicitCapacity这两个方法

1
2
3
4
5
6
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

该方法确认minCapacity,第一次扩容为 10 (还没扩容)

1
2
3
4
5
6
7
private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
  1. modCount记录集合被修改的次数,防止多线程操作出现异常
  2. if (minCapacity - elementData.length > 0)这句判断数组大小是否足够,如果elementData的大小不够,就会调用grow方法,正真的扩容数组。
1
2
3
4
5
6
7
8
9
10
11
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
  1. 使用扩容机制来确定要扩容到多大
  2. 第一次newCapacity为 10
  3. 第二次及其以后,按照 1.5 倍扩容
  4. 扩容使用的是Arrays.copyOf()

Vector

  1. Vector底层也是一个对象数组,protected Object[] elementData;
  2. Vector是线程同步的,Vector类的操作方法带有synchronized

无参构造器

1
2
3
public Vector() {
this(10);
}

默认给一个 10 传到有参构造器中

有参构造器

1
2
3
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
1
2
3
4
5
6
7
8
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}

底层扩容机制

Vector底层扩容机制与ArrayList大同小异,差别主要是体现在grow方法

1
2
3
4
5
6
7
8
9
10
11
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}

capacityIncrement默认为 0,也就是说默认扩容成原来的两倍

Vector底层结构和ArrayList比较

底层结构 版本 线程安全(同步)/效率 扩容倍数
ArrayList 可变数组 jdk1.2 不安全;效率高 如果有参构造则为1.5倍;如果是无参构造,第一次为10,第二次后为1.5倍扩容
Vector 可变数组 jdk1.0 安全;效率不高 如果指定大小则每次按2倍扩容;如果是无参构造,默认为10,满后就按2倍扩容

HashSet

HashSet 实现了Set接口,其实际底层为HashMapHashMap的底层是(数组+链表+红黑树)

1
2
3
public HashSet() {
map = new HashMap<>();
}

HashSet不保证元素是有序的,取决于 hash 后,在确定索引的结果.

模拟HashSet(没用使用红黑树)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class HashSet02 {
public static void main(String[] args) {
// 模拟一个HashSet底层

// 创建一个数组,数组的类型是 Node[]
// 有些人直接把 Node[] 数组称为 表
Node[] table = new Node[16];
System.out.println(table);
// 创建节点
Node john = new Node("john", null);
table[2] = john;
System.out.println(table);

Node jack = new Node("jack", null);
john.next = jack;

Node ross = new Node("Ross", null);
jack.next = ross;
System.out.println(table);

Node luck = new Node("luck", null);
table[3] = luck;
}
}

class Node {
Object item;
Node next;

public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
}

在 java8 中,如果有一条链表的元素个数到达 TREELFY_THRESHOLD(默认是8),并且table >= MIN_TREELFY_CAPACITY(默认64),就会进行树化(红黑树)

HashSet底层机制

  1. HashSet底层使HashMap,第一次添加时,table 数组扩容到 16 ,临界值(threshold) 是 16 * 加载因子(loadFactor) 是 0.75 = 12
  2. 如果 table 数组使用到了临界值 12 就会扩容到 16 * 0.75 = 24,依次类推
  3. 在 Java8 中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是 8),并且 tavle 的大小 >= MIN_THREELFY_CAPACITY(默认是 64),就会进行树化(红黑树),否则仍会采用数组扩容机制

HashMap & Hashtable

1.HashMap

HashMap 是以 key-value 对的方式来储存数据(HashMap$Node 类型)
key 不能重复,但只可以重复,允许使用 null 键和 null
如果添加相同的 key 则会覆盖掉原来的 key-value,等同于修改(key 不会替换,value 会替换)
HashSet 一样,不保证映射的顺序,因为底层是以 hash 表的方式来存储的
HashMap 没有实现同步,因此是线程不安全的

2.Hashtable

存放的元素是键值对:即 K-V
Hashtable 的键和值都不能为 null,否则会抛出 NullPointerException
Hashtable 使用方法与 HashMap 基本一致
Hashtable 是线程安全的(synchronized),HashMap是线程不安全的

版本(出现时间) 线程安全(同步) 效率 允许 null 键 null 值
HashMap 1.2 不安全 允许
Hashtable 1.0 安全 较低 不允许

Properties

Properties类继承自 Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据,器使用特点与Hashtable类似

Properties还可以用于 从 xxx.properties 文件中,加载数据到Porperties类对象并进行读取和修改

xxx.properties 通常作为配置文件

练习题

1.下列代码会不会报异常

1
2
TreeSet set = new TreeSet();
set.add(new Person());

因为 TreeSet() 构造器中没有传入 Comparator 接口的匿名内部类,所以底层将 Person 转成 Comparable类型 Comparable<? super K> k = (Comparable<? super K)> key;。但Person类没有实现Comparator接口,不能转换,所以会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
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);
}

2.下面的代码输出什么?

已知:Person 类按照 id 和 name 重写了 hashCode 和 equals 方法

1
2
3
4
5
6
7
8
9
10
11
12
HashSet set = new HashSet();
Person p1 = new Person(1001, "AA");
Person p2 = new Person(1002, "BB");
set.add(p1);
set.add(p2);
p1.name = "CC";
set.remove(p1);
System.out.println(set);
set.add(new Person(1001, "CC"));
System.out.println(set);
set.add(new Person(1001, "AA"));
System.out.println(set);

第一个输出有两个元素:因为p1.name = "CC";改变了name所以set.remove(p1);中早不到其原来的位置,所以删除不了

第二个输出有三个元素:因为set.add(new Person(1001, "CC"));hashCode的值与之前存进去的p1不一样

第三个输出有四个元素:因为set.add(new Person(1001, "AA"));hashCode的值与p1不一样

泛型

1
2
3
4
5
6
7
8
9
10
public static void func1(List<?> c) {

}
public static void func2(List<? extends AA> c) {

}
public static void func3(List<? super AA> c) {

}

List<?>表示任意的泛型都可以接受

List<? extends AA>表示 上限,可以接受 AA 或者 AA 的子类

List<? super AA>表示 上限,可以接受 AA 类以及 AA 的父类

线程

线程的生命周期

image-20220124094446032

互斥锁

同步方法如果没有使用 static 修饰:默认对象为 this

如果方法使用 static 修饰,默认对象为 当前类.class

1
2
3
4
5
6
7
8
9
10
11
class A {
public static synchronized void m1() {
...
}

public static void m2 {
synchronized(A.class) {
...
}
}
}