代码块

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

  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倍扩容