Java基础总结篇

关于Java基础方面的总结,快四个月没写过Java了,回忆一下

Java基础

数据类型

基本类型

八大基本类型

基本类型占用空间(bit)默认值取值范围
byte80[-2^7, 2^7-1]
char(无符号)16‘\u0000’[0, 2^16-1]
short160[-2^15, 2^15-1]
int320[-2^31, 2^31-1]
float320.0f~
long640L[-2^63, 2^63-1]
double640.0d~
boolean~falsetrue / false

boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 支持 boolean 数组,但是是通过读写 byte 数组来实现的。

包装类型

基本类型和包装类型对应表

注意:所有的包装类型都是final的,也就是不可继承

基本类型包装类型
byteByte
charCharacter
shortShort
intInteger
floatFloat
longLong
doubleDouble
booleanBoolean

基本类型都有包装类型,基本类型和包装类型之间的赋值使用自动装箱和拆箱来完成。

1
2
Integer x = 2;     // 装箱 调用了 Integer.valueOf(2)
int y = x; // 拆箱 调用了 X.intValue()

缓存池

Java中存在一个缓存池这个东西,就是当我们使用valueOf的时候,它会优先从缓存池中获取对象

例子:

1
2
3
4
5
6
7
8
9
Integer a = Integer.valueOf(123);
Integer b = Integer.valueOf(123);
Integer c = new Integer(123);
System.out.println(a == b); // true
System.out.println(b == c); // false

Integer d = Integer.valueOf(1234);
Integer e = Integer.valueOf(1234);
System.out.println(d == e); // false

由此可见,确实是存在缓存池这个东西,并且这个还是有大小限制的,123行,但是1234就没在缓存池中

查看valueOf源码:

1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

lowhigh之间从缓存池中拿,其他情况直接new,这个lowhigh是多少呢?

low是-128,high是127,并且,缓存池最大值还是可以用jvm指定(-XX:AutoBoxCacheMax=<size>),并且他的逻辑是从指定的缓存池最大值和127取最大,也就是我们就算设置high为126,他的缓存池最大也是127。

注意:1.8所有数值类缓存池中,只有Integer的缓存值上界可调!

基本类型对应的缓冲池如下:

  • boolean values true and false
  • all byte values
  • short values between -128 and 127
  • int values between -128 and 127
  • char in the range \u0000 to \u007F

String

概述

String被声明为final,因此不可继承。

java8中String内部使用char数组存数据:

1
2
3
4
5
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}

java9更换为了字节数组,并且用coder来标志用的哪种编码:

1
2
3
4
5
6
7
8
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final byte[] value;

/** The identifier of the encoding used to encode the bytes in {@code value}. */
private final byte coder;
}

String中存具体数据的value数组被声明为了final,代表value不能引用其他数组了,并且String内部没有改变value数组的方法,所以可以保证String不可变。

为啥要把String搞成不可变的?

  1. 字符串池的需要:在Java中有一个字符串池,如果重复创建同一个字符串对象的话,第二次就会从字符串池里面找这个对象,然后返回此对象的引用地址,如果String要是可变的,那这个就不能用了,因为你正在引用的对象竟然可以在你不知道的情况下被更改…….

  2. 缓存哈希值:String的哈希值在Java中经常被使用,例如作为HashMap的key,不可变的特性可以让String只会算一遍哈希,然后后面再用就不用重复计算这个字符串的哈希了,提高效率,下面是存字符串哈希值的属性相关代码(Cache the hash code for the string):

    1
    2
    3
    4
    5
    6
    7
    8
    public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0
    }
  3. 安全考虑:String 被广泛用作许多 java 类的参数,例如网络连接、打开文件等。如果 String 不是不可变的,则连接或文件将被更改,这可能会导致严重的安全威胁。

  4. 线程安全:String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

String、StringBuffer、StringBuilder

可变性

  1. String不可变
  2. StringBuffer和StringBuilder可变

线程安全

  1. String不可变,线程安全
  2. StringBuffer内部使用synchronized进行同步,线程安全
  3. StringBuilder线程不安全

使用场景:

  1. 如果字符串后续不会更改,就用String

    注意:使用 String 进行逻辑操作相当慢,根本不建议使用,因为 JVM 将 String 转换为字节码中的 StringBuffer。大量开销被浪费在从 String 转换为 StringBuffer 然后再转换回 String 上

  2. 如果字符串有大量逻辑和操作,就是那种改来改去的,就要使用StringBuffer或StringBuilder了

    • 如果程序只能单线程访问,直接用StringBuilder效率高,因为没有加锁
    • 如果程序可以多线程访问,用StringBuffer,线程安全

字符串常量池

在编译期间,所有字符串字面量都会加到字符串常量池中,也可以用intern()方法把运行期间的字符串加入常量池

啥是字面量:String a = "hello",这个hello就是字面量,会加到常量池里面,重复创建返回都都是这个hello的引用

例子:

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

在运行期间,可以用intern()方法:常量池如果存在这个字符串就返回引用,不存在就往常量池里放入再返回引用

例子:

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

注意:在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。

运算

参数传递:Java中只有值传递,没有引用传递!

Java 的参数是以值传递的形式传入方法中,而不是引用传递,如果是以对象作为方法参数传入方法中,传的其实是对象的地址以值得形式传入方法中。

其实,就是传值的时候把地址传进去了,里面那个参数指向了那个地址的对象,他可以修改对象本身的属性值,但是他不能改变外部的值的指向

隐式类型转换

Java不能隐式向下执行转型,因为会丢失精度,+=++可以隐式向下转型

float和double

如1.1默认是double

1
2
3
4
5
6
7
8
// 1.1字面量是double类型
float a = 1.1;
// 需要加f表示
float a1 = 1.1f;

a1 = a1 + 1.1;
// += 可以隐式的向下转换
a1 += 1.1; // 相当于a1 = (float) (a1 + 1.1);

short和int

1
2
3
4
5
6
7
8
9
// 如果1小的话是可以转short,如果大数的话会提示数字过大无法赋值
short b = 1;
short c = 123456789;
// 1 字面量是int
b = b + 1;
// += 或 ++ 可以隐式的向下转换
b += 1; // 相当于 b = (short) (b + 1);
b++; // 同上
b = (short) (b + 1);

switch

java7开始switch支持String类型

1
2
3
4
5
6
7
8
9
10
11
12
String s = "a";

switch (s) {
case "a":
System.out.println("a");
break;
case "b":
System.out.println("b");
break;
default:
System.out.println("not found");
}

switch 不支持 long、float、double,是因为 switch 的设计初衷是对那些只有少数几个值的类型进行等值判断,如果值过于复杂,那么还是用 if 比较合适。

关键字 TODO

final

final修饰符一般用于基本类型(primitive)域,或不可变(immutable)类对象。

  1. 声明数据

    声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量

    • 对于基本类型(基本八个类型int、flot…),声明后数值不能改变
    • 对于引用类型(对象),声明后不能改变引用,也就是不能再引用其他对象了,但是被引用的对象本身是可以修改的
  2. 声明方法

    声明方法,方法不能被子类重写

    private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。

  3. 声明类

    声明类,类不能被继承

static

  1. 修饰变量:静态变量

    修饰变量是静态变量

    • 静态变量:又叫类变量,这个变量是属于类的,可以直接通过类名来访问,类的所有实例都共享静态变量,静态变量在内存中只存在一份
    • 实例变量:每创建一个实例,就会创建一个实例变量,与实例共生死
  2. 修饰方法:静态方法

    静态方法在类加载的时候就存在了,不依赖于任何实例,所以静态方法必须有实现,也就是静态方法不能是抽象方法;并且,静态方法内部,只能访问静态字段和静态方法,方法中不能有this和super关键字,因为这俩关键字是和对象关联的。

  3. 静态代码块

    可以用static加花括号,来声明一个静态代码块,这个只在类初始化的时候运行一次

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Student {

    static {
    System.out.println("static block");
    }

    public static void main(String[] args) {
    Student student = new Student();
    Student student1 = new Student();
    Student student2 = new Student();
    }
    }
    // 结果,只会输出一次static block
    static block
  4. 静态内部类

    首先说普通内部类,普通内部类创建的时候,需要依赖外部类的具体实例对象才能创建:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class OuterClass {

    class InnerClass {
    }

    static class StaticInnerClass {
    }

    }

    // 非静态内部类不能通过类直接创建 OuterClass' is not an enclosing class
    OuterClass.InnerClass innerClass = new OuterClass.InnerClass(); // error
    // 非静态内部类只能通过外部类的实例来创建
    OuterClass outerClass = new OuterClass();
    OuterClass.InnerClass innerClass1 = outerClass.new InnerClass();

    而静态内部类,可以直接创建

    1
    2
    // 静态内部类可以直接new,不需要依赖具体外部类实例
    OuterClass.StaticInnerClass staticInnerClass = new OuterClass.StaticInnerClass();
  5. 静态导包

    在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。

    1
    import static com.xxx.ClassName.*
  6. 初始化顺序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public static String staticField = "静态变量";

    static {
    System.out.println("静态语句块");
    }

    public String field = "实例变量";

    {
    System.out.println("普通语句块");
    }

    public InitialOrderTest() {
    System.out.println("构造函数");
    }

    存在继承的情况下,初始化顺序为:先静态(先父类,再子类),再实例(先父类,再子类),先变量,普通语句块,再构造函数

    • 父类(静态变量、静态语句块)
    • 子类(静态变量、静态语句块)
    • 父类(实例变量、普通语句块)
    • 父类(构造函数)
    • 子类(实例变量、普通语句块)
    • 子类(构造函数)

this

this表示当前类的实例,可以做以下几件事:

  1. this关键字可用来引用当前类的实例变量。

  2. this关键字可用于调用当前类方法(隐式)。

  3. this()可以用来调用当前类的构造函数。

  4. this关键字可作为调用方法中的参数传递。

  5. this关键字可作为参数在构造函数调用中传递。

  6. this关键字可用于从方法返回当前类的实例。

其实只要记住this就是当前类的实例对象就行,想咋操作就咋操作

参考:

  1. https://www.yiibai.com/java/this-keyword.html
  2. https://docs.oracle.com/javase/tutorial/java/javaOO/thiskey.html

Object通用方法

概览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public native int hashCode()

public boolean equals(Object obj)

protected native Object clone() throws CloneNotSupportedException

public String toString()

public final native Class<?> getClass()

protected void finalize() throws Throwable {}

public final native void notify()

public final native void notifyAll()

public final native void wait(long timeout) throws InterruptedException

public final void wait(long timeout, int nanos) throws InterruptedException

public final void wait() throws InterruptedException

equals()

  1. 等价关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    x.equals(x); // true

    x.equals(y) == y.equals(x); // true

    if (x.equals(y) && y.equals(z))
    x.equals(z); // true;

    x.equals(y) == x.equals(y); // true

    x.equals(null); // false;
  2. 等价和相等(equals和==)

    • 对于基本类型:== 判断两个值是否相等,基本类型没有equals方法
    • 对于引用类型:== 判断两个值是否引用自同一个对象,equals判断对象是否等价
    1
    2
    3
    4
    Integer x = new Integer(1);
    Integer y = new Integer(1);
    System.out.println(x.equals(y)); // true
    System.out.println(x == y); // false
  3. 实现equals方法的一般逻辑

    • 检查是否是同一个对象的引用,如果是直接返回true
    • 检查是否是同一个类型,如果不是直接返回false
    • 将object对象进行转型(上一步已经检查过是否是同一个类型了,所以这里直接转型没问题)
    • 判断每个关键域是否相等
    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
    public class Example {

    private String a;

    private Integer b;

    @Override
    public boolean equals(Object o) {
    if (this == o) {
    return true;
    }
    if (!(o instanceof Example)) {
    return false;
    }
    Example example = (Example) o;
    return Objects.equals(a, example.a) && Objects.equals(b, example.b);
    }

    @Override
    public int hashCode() {
    return Objects.hash(a, b);
    }
    }

    // 关于Objects.equals(a, b)
    public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
    }

hashCode()

hashCode() 返回哈希值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价,这是因为计算哈希值具有随机性两个值不同的对象可能计算出相同的哈希值

在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象哈希值也相等。

HashSet 和 HashMap 等集合类使用了 hashCode() 方法来计算对象应该存储的位置,因此要将对象添加到这些集合类中,需要让对应的类实现 hashCode() 方法。

重写hashCode方法可以用Objects.hash()

1
2
3
4
5
@Override
public int hashCode() {
// a b 是类的私有属性
return Objects.hash(a, b);
}

也可以:

理想的哈希函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的哈希值上。这就要求了哈希函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。

R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位,最左边的位丢失。并且一个数与 31 相乘可以转换成移位和减法:31*x == (x<<5)-x,编译器会自动进行这个优化。

1
2
3
4
5
6
7
8
@Override
public int hashCode() {
int result = 17;
result = 31 * result + x;
result = 31 * result + y;
result = 31 * result + z;
return result;
}

toString()

默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。

1
2
3
Example example = new Example("1", 1);
System.out.println(example.hashCode()); // 2481
System.out.println(example.toString()); // com.hc.basics.Example@9b1

9b1就是2481的16进制:9*16^2 + 11 * 16 + 1 = 2481

clone()

clone()是Object类下的protected方法,这个类不显示的去重写clone()方法,其他类就不能直接调用

方法的作用就是复制一个对象,可以参考1.8API文档:

clone()方法可以保证:

  • x.clone() != x:代表是正儿八经复制的个对象,在堆中有这个实例的一片地方
  • x.clone().getClass() == x.getClass():保证类型相同
  • x.clone().equals(x):保证复制后的对象的(字段)内容和之前的相同

并且下面也说了:**clone()方法是浅拷贝,不是深拷贝**

注意:重写了clone()方法后,如果这个方法不实现Cloneable接口,就会抛出CloneNotSupportedException异常,这个接口的作用就是打个标记,证明我这个类可以克隆,可以理解为一个约定

浅拷贝

浅拷贝就是只复制对象,但是对象的属性,是直接通过=号赋值,也就是对象是新建的,但是里面的属性都是复制的引用

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Student {

private String name;

public Student() {
}

public Student(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class Teacher implements Cloneable {

private String name;

private String subject;

/** 班长 */
private Student classPresident;

public Teacher() {
}

public Teacher(String name, String subject, Student classPresident) {
this.name = name;
this.subject = subject;
this.classPresident = classPresident;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getSubject() {
return subject;
}

public void setSubject(String subject) {
this.subject = subject;
}

public Student getClassPresident() {
return classPresident;
}

public void setClassPresident(Student classPresident) {
this.classPresident = classPresident;
}

@Override
protected Teacher clone() throws CloneNotSupportedException {
return (Teacher) super.clone();
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
Teacher teacher = new Teacher("a", "math", new Student("b"));
try {
Teacher clone = teacher.clone();
System.out.println(teacher == clone);
System.out.println(teacher.getName() == clone.getName());
System.out.println(teacher.getSubject() == clone.getSubject());
System.out.println(teacher.getClassPresident() == clone.getClassPresident());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}

看看这俩student一样不:

1
2
3
4
false
true
true
true

可以看到,clone的对象确确实实是新建的,而克隆的对象的字段,确实是复制的引用,因为都是指向了同一个对象。

深拷贝

深拷贝就是完全把字段的属性也拷贝过来,而不是直接复制引用,重写clone()方法

1
2
3
4
5
6
7
8
@Override
protected Teacher clone() throws CloneNotSupportedException {
Teacher teacher = (Teacher) super.clone();
teacher.name = new String(this.name);
teacher.subject = new String(this.subject);
teacher.classPresident = new Student(this.classPresident.getName());
return teacher;
}

再次运行main方法:

1
2
3
4
false
false
false
false

这也不是完全的深拷贝,因为classPresident的name也是引用过来的,所以要在一个复杂的对象里实现真正的深拷贝是非常困难的

注意:String是不可变,我们再new一下意义不大,但是classPresident是可变的,如果不new就是公用一个对象,对象内容可能被克隆出来的副本改变了,影响到了原有的teacher

使用

我们一般不用clone()方法来拷贝一个对象,既复杂(发生在子类的克隆需要链式调用父类的克隆)又有风险,还会抛出异常,还得类型转换,所以可以使用一个拷贝构造函数或者拷贝工厂来实现:

拷贝构造函数(在Teacher类中添加如下构造函数):

1
2
3
4
5
public Teacher(Teacher original) {
this.name = new String(original.name);
this.subject = new String(original.subject);
this.classPresident = new Student(original.classPresident.getName());
}

测试:

1
2
3
4
5
6
Teacher teacher = new Teacher("a", "math", new Student("b"));
Teacher clone = new Teacher(teacher);
System.out.println(teacher == clone);
System.out.println(teacher.getName() == clone.getName());
System.out.println(teacher.getSubject() == clone.getSubject());
System.out.println(teacher.getClassPresident() == clone.getClassPresident());

结果:

1
2
3
4
false
false
false
false

可以看出来拷贝构造函数确实很方便,不用抓异常,不用类型转换啥的

拷贝工厂(在Teacher类中添加如下静态方法):

1
2
3
4
5
6
7
public static Teacher newInstance(Teacher original) {
Teacher teacher = new Teacher();
teacher.name = new String(original.name);
teacher.subject = new String(original.subject);
teacher.classPresident = new Student(original.classPresident.getName());
return teacher;
}

测试:

1
2
3
4
5
6
Teacher teacher = new Teacher("a", "math", new Student("b"));
Teacher clone = Teacher.newInstance(teacher);
System.out.println(teacher == clone);
System.out.println(teacher.getName() == clone.getName());
System.out.println(teacher.getSubject() == clone.getSubject());
System.out.println(teacher.getClassPresident() == clone.getClassPresident());

结果:

1
2
3
4
false
false
false
false

缺点

以下是一些缺点,因为许多开发人员不使用Object.clone()

  1. 使用Object.clone()方法要求我们在代码中添加大量语法,如实现Cloneable接口,定义clone()方法和处理CloneNotSupportedException,最后调用Object.clone()并将其转换为对象。
  2. Cloneable接口缺少clone()方法,实际上Cloneable是一个标记接口,并且没有任何方法,我们仍然需要实现它只是告诉JVM我们可以对我们的对象执行clone()。
  3. Object.clone()受到保护,因此我们必须提供自己的clone()并从中间接调用Object.clone()。
  4. 我们对对象构造没有任何控制,因为Object.clone()不会调用任何构造函数。
  5. 如果我们在子类中编写克隆方法,例如 然后,所有人的超类应该在其中定义clone()方法,或者从另一个父类继承它,否则super.clone()链将失败。
  6. Object.clone()仅支持浅拷贝,因此我们新克隆对象的引用字段仍将保存原始对象的哪些字段所持有的对象。 为了克服这个问题,我们需要在我们的类所持有的每个类中实现clone(),然后在我们的clone()方法中单独调用它们,如下例所示。
  7. 我们无法操作Object.clone()中的final字段,因为最终字段只能通过构造函数进行更改。 在我们的例子中,如果我们希望每个Person对象都是id唯一的,那么如果我们使用Object.clone(),我们将获得重复的对象,因为Object.clone()不会调用构造函数,并且最终的最终id字段不能被修改 来自Person.clone()。

复制构造函数优于Object.clone(),因为它们

  1. 不要强迫我们实现任何接口或抛出任何异常,但如果需要,我们肯定可以这样做。
  2. 不要求任何演员阵容。
  3. 不要求我们依赖于未知的对象创建机制。
  4. 不要求父类遵守任何合同或实施任何内容。
  5. 允许我们修改最终字段。
  6. 允许我们完全控制对象创建,我们可以在其中编写初始化逻辑。

参考:

  1. https://www.itranslater.com/qa/details/2130931082093659136
  2. https://xiaoyue26.github.io/2017/03/03/%E6%8B%B7%E8%B4%9D%E5%B7%A5%E5%8E%82/
  3. http://www.cyc2018.xyz/Java/Java%20%E5%9F%BA%E7%A1%80.html#clone
  4. https://docs.oracle.com/javase/8/docs/api/
  5. https://www.jianshu.com/p/41602eeb0ad5

继承

访问修饰符

  • 默认什么也不加(类、接口、方法、变量):同一包内可见,外边包无法引入
  • private(变量、方法):同一类内可见,可以修饰内部类,不能修饰外部类
  • public(类、接口、方法、变量):所有类可见
  • protected(变量、方法):同一包内的类可见,子类也可见;可以修饰内部类,不能修饰外部类

特性

  • 子类拥有父类非 private 的属性、方法
  • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展
  • 子类可以用自己的方式实现父类的方法(重写)
  • Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 B 类继承 A 类,C 类继承 B 类,所以按照关系就是 B 类是 C 类的父类,A 类是 B 类的父类,这是 Java 继承区别于 C++ 继承的一个特性
  • 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)

抽象类和接口

  1. 抽象类

    抽象类和抽象方法都用abstract关键字进行声明,如果一个类中包含抽象方法,那么这个类必须声明为抽象类。

    抽象类和普通类最大的区别就是,抽象类不能被实例化,只能被继承。

  2. 接口

    接口是抽象类的延伸,在Java8之前,可以看做是个完全抽象的类,也就是说不能有任何方法的实现。

    从java8开始,接口可以拥有默认方法的实现,因为不支持默认方法的接口维护成本太高了,加一个方法,所有的实现类都需要实现。

    接口的成员(字段+方法)默认都是public的,并且不允许定义为private或protected的。

    从java9开始,允许将方法定义为private,这样就能定义某些复用的代码,并且还不会将方法暴露出去。

    接口字段默认是static final的。

  3. 对比

    • 从设计层面上来看,抽象类提供了一种IS-A的关系,需要满足里氏替换原则,子类必须能替换所有父类对象。而接口更是一种LIKE-A的关系,提供方法的实现契约,不要求接口和接口实现的类有IS-A的关系。
    • 从使用上来看,一个类可以实现多个接口,但不能继承多个抽象类
    • 接口的字段必须是static和final的,而抽象类无限制
    • 接口成员只能是public,抽象类的成员可以有多种访问权限
  4. 使用选择

    使用接口:

    • 需要让不相关的类都实现一个方法,例如不相关的类都实现Comparable的compareTo() 方法
    • 需要使用多继承

    使用抽象类:

    • 需要在几个类中共享代码
    • 需要能控制成员的访问权限
    • 需要继承非静态和非常量字段

    很多情况下接口优于抽象类,没有抽象类的层次要求,可以灵活的为一个类添加行为

super

两种用途

  • 访问父类的构造函数:可以使用super()来访问父类构造函数,从而委托父类完成一些初始化的工作。应该注意到,子类一定会调用父类的构造函数来完成初始化工作,一般是调用父类的默认构造函数,但是也可以使用super()来有选择的调用其他的构造函数
  • 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现

重写和重载

  1. 重写(Override)

    存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法

    为了满足里氏替换原则,重写有以下三个限制:

    • 子类方法的访问权限必须大于等于父类
    • 子类的返回值类型必须是父类的方法返回类型或子类型
    • 子类抛出的异常范围必须是父类抛出的异常类型或子类型

    使用@Override注解,可以让编译器帮忙检查是否满足以上三个条件

    注意:调用方法的时候,先从本类找有没有这个方法,再去父类找,如果都没有,那就要对参数进行转型,转成父类之后看看是否有对应的方法,总的来说方法调用优先级为:

    • this.func(this)
    • super.func(this)
    • this.func(super)
    • super.func(super)
  2. 重载(Overload)

    存在于同一个类中,指一个方法与已经存在的方法名称相同,但是参数的个数,类型,顺序至少有一个不同

    返回值不同,但其他相同,这不叫重载

反射

概述

反射是Java的特性之一,允许程序在运行时获取自身的信息,并操作类或对象的的内部属性。

官方解释:

Reflection is a feature in the Java programming language. It allows an executing Java program to examine or “introspect” upon itself, and manipulate internal properties of the program. For example, it’s possible for a Java class to obtain the names of all its members and display them.

One tangible use of reflection is in JavaBeans, where software components can be manipulated visually via a builder tool. The tool uses reflection to obtain the properties of Java components (classes) as they are dynamically loaded.

翻译:

反射是 Java 编程语言中的一个特性。它允许正在执行的 Java 程序检查或“自省”自身,并操纵程序的内部属性。例如,Java 类可以获取其所有成员的名称并显示它们。

反射的一种实际用途是在 JavaBeans 中,其中软件组件可以通过构建器工具进行可视化操作。该工具使用反射来获取动态加载的 Java 组件(类)的属性。

反射的核心是JVM在运行时才动态加载类或调用方法/访问属性,不需要事先在编译期就确定运行对象是谁。

反射主要提供以下功能:

  • 运行时判断任意一个对象所属的类
  • 运行时构造任意一个类的对象
  • 运行时获取任意一个类所具有的的成员变量和方法(甚至可以获取私有方法)
  • 运行时任意调用一个对象的方法

是运行时,而不是编译时

其实学习反射主要是了解反射相关的APi就行了

获取Class对象

  1. 使用Class.forName(“className”)加载类,比如加载jdbc的数据库驱动,加载的时候会执行类中的静态代码块中的内容,从而把数据库驱动注册到DriverManager中,来连接数据库

    1
    Class.forName("com.mysql.cj.jdbc.Driver");
  2. 对于对象,可以使用Object类的,getClass()方法

    1
    2
    Example example = new Example();
    Class<? extends Example> aClass = example.getClass();
  3. 对于类,可以直接.class

    1
    2
    Class<Integer> intClass = int.class;
    Class<? extends Integer> integerClass = Integer.class;

汇总

1
2
3
4
5
6
7
Example a = new Example();
// 通过调用对象的getClass来获取
Class<? extends Example> aClass = a.getClass();
// 直接类名.class
Class<Example> clazz = Example.class;
// 传全限定类名
Class<?> aClass1 = Class.forName("com.hc.basics.Example");

判断某个对象是否是某个类的实例

使用Class类的isInstance(Object o)方法,方法签名如下

例子:

1
2
3
4
5
Example a = new Example();
Example b = new Example();
Class<? extends Example> aClass = a.getClass();
// 判断b对象是否是Example类的实例
System.out.println(aClass.isInstance(b));

使用Class对象创建实例

创建实例肯定是需要一个构造方法的,分别是无参和带参的构造方法:

  • 无参构造:直接使用class实例的newInstance()方法
  • 带参构造:需要先获取构造器clazz.getConstructor(Integer.class, String.class),是靠传入参数的顺序来判断使用哪个构造函数的,然后用构造器newInstance()创建实例
1
2
3
4
5
6
7
8
9
10
11
12
// 使用Example的Class对象创建Example实例
Class<Example> clazz = Example.class;
Example example = null;
try {
example = clazz.newInstance();
System.out.println("无参构造:" + example);
Constructor<Example> constructor = clazz.getConstructor(Integer.class, String.class);
example = constructor.newInstance(1, "jack");
System.out.println("带参构造:" + example);
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}

获取某个类的所有方法

  • getDeclaredMethods 方法返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法

    可以理解为只要是当前类里面写的方法,全部都拿出来

    1
    public Method[] getDeclaredMethods() throws SecurityException
  • getMethods 方法返回某个类的所有公用(public)方法,包括其继承类的公用方法

    也就是会把Object类的所有方法也都搞出来

    1
    public Method[] getMethods() throws SecurityException
  • getMethod 方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象。

    1
    public Method getMethod(String name, Class<?>... parameterTypes)

获取类字段信息

  • getFiled:访问公有的成员变量

  • getDeclaredField:所有已声明的成员变量,但不能得到其父类的成员变量

  • getFiledsgetDeclaredFields 方法用法同上(参照 Method)

调用方法

使用Method类中的invoke方法进行实际调用,需要传入实例和方法参数,返回是方法执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用Example的Class对象创建Example实例
Class<Example> clazz = Example.class;
Example example = null;
try {
// 先创建个对象
example = clazz.newInstance();
// clazz.getMethod 获取指定方法
Method introduceMethod = clazz.getMethod("introduce", Integer.class, String.class);
// 调用
Object result = introduceMethod.invoke(example, 1, "jack");
System.out.println(result);
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}

优缺点

优点:

  • 可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
  • 类浏览器和可视化开发环境 :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
  • 调试器和测试工具 : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。

缺点:

  • 性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
  • 安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
  • 内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。

异常

概述

Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: ErrorException。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种:

  • 受检异常 :需要用 try…catch… 语句捕获并进行处理,并且可以从异常中恢复;在正确的程序运行过程中,很容易出现的、情理可容的异常状况,在一定程度上这种异常的发生是可以预测的,并且一旦发生该种异常,就必须采取某种方式进行处理。
  • 非受检异常 :是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复;包括RuntimeException及其子类和Error

不受检查异常为编译器不要求强制处理的异常,检查异常则是编译器要求必须处置的异常。

嵌套

多个try块嵌套,优先在距离最近的catch块处理,如果当前catch不能能处理,则到上一级try的catch块处理

finally

finally会在return前执行,不要再finally中return!

如果在finally中修改方法中的变量,对于基本类型,无法修改,对于引用类型,可以修改引用类型的内容

finally中经常干的事情就是释放资源

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static int testException() {
int i = 0;
try {
i = 1;
return i;
} catch (ArithmeticException e) {
e.printStackTrace();
} finally {
i = 6;
}
return i;
}

public static void main(String[] args) {
System.out.println(testException());
}
// output
1

引用类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static People testException() {
People people = new People();
people.setName("aa");
try {
people.setAge(5);
return people;
} catch (ArithmeticException e) {
e.printStackTrace();
} finally {
people.setName("bb");
people.setAge(6);
}
return people;
}

public static void main(String[] args) {
System.out.println(testException());
}
// output
People{name='bb', age=6}

泛型

概述

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

注意:泛型只是在编译时限制的,在运行时会擦除,仅用做编译时限制开发人员别放错类型了

下面这个是泛型类,泛型类与泛型接口都差不多,都是类名 + <T>,尖括号里面的字母写啥都行,只不过为了让人看懂,衍生出了,T、K、V、E、N、?

  • E - Element (在集合中使用,因为集合中存放的是元素)
  • T - Type(Java 类)
  • K - Key(键)
  • V - Value(值)
  • N - Number(数值类型)
  • - 表示不确定的 java 类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Box<T> {
private T t;

public void set(T t) {
this.t = t;
}

public T get() {
return t;
}

/**
* 泛型方法
*/
public <K,V> T test(T t, K k, V v) {
System.out.println("K: " + k + " V: " + v + " T: " + t);
return t;
}
}

泛型方法

上面的get和set是泛型方法吗?不是

test才是泛型方法

泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型

1
2
3
4
5
6
7
/**
* 泛型方法
*/
public <K,V> T test(T t, K k, V v) {
System.out.println("K: " + k + " V: " + v + " T: " + t);
return t;
}

说明:

  • 上面的public 和 T 中间的 <K,V>可以理解为声明此方法是个泛型方法,并且会用到K、V这俩类型
  • 只有这样声明了,才是泛型方法,没用这样声明的都不是,一般这个声明后面紧跟返回参数类型
  • 这个T为啥不声明呢?是因为这个T是泛型类声明的泛型,所以这里可以直接用,不用再次声明

使用

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
Box<Integer> box = new Box<>();
box.set(666);
System.out.println(box.get());
Integer haha = box.test(111, "haha", 333);
System.out.println(haha);
}
// output
666
K: haha V: 333 T: 111
111

泛型方法与可变参数

1
2
3
4
5
6
7
8
9
public static <T> void printMsg(T... args) {
for (T arg : args) {
System.out.println(arg);
}
}

public static void main(String[] args) {
printMsg(1, 2, 3, "haha", "hehe");
}

tips:

无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而已,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。

泛型的上下边界

总结

在Java泛型定义时:

等大写字母标识泛型类型,用于表示未知类型。
用<T extends ClassA & InterfaceB …>等标识有界泛型类型,用于表示有边界的未知类型。
在Java泛型实例化时:

用<?>标识通配符,用于表示实例化时的未知类型。
用<? extends 父类型>标识上边界通配符,用于表示实例化时可以确定父类型的未知类型。
用<? super 子类型>标识下边界通配符,用于表示实例化时可以确定子类型的未知类型。

上边界

定义:<T extends Number>表示必须是Number的子类才行

1
2
3
4
5
public static <T extends Number> void printMsg(T... args) {
for (T arg : args) {
System.out.println(arg);
}
}

如果不是,编译会报错

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
printMsg(1, 2, 3, "haha", "hehe");
}

// 编译报错
java: 无法将类 com.hc.demo.TestBox中的方法 printMsg应用到给定类型;
需要: T[]
找到: int,int,int,java.lang.String,java.lang.String
原因: 推断类型不符合上限
推断: java.lang.Object&java.io.Serializable&java.lang.Comparable<? extends java.lang.Object&java.io.Serializable&java.lang.Comparable<?>>
上限: java.lang.Number

下边界

注意:下边界定义时不能限制,只能实例化时限制

这个限定了只能放People的子类,放其他类如cat就不行,限定了下界

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Box<? super People> box = new Box<>();
box.set(new People());
box.set(new Student());
box.set(new Cat()); // error
}

// output
java: 不兼容的类型: com.hc.demo.Cat无法转换为capture#1, 共 ? super com.hc.demo.People

不得不说泛型数组

在Java中不允许创建确切类型的泛型数组

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
List<String>[] lsa = new List<String>[10]; // Not really allowed.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error: ClassCastException.
}

除非是采用通配符的方式

1
2
3
4
5
6
7
List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.    
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Correct.
Integer i = (Integer) lsa[1].get(0); // OK

注解

是什么

Java注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。包含在 java.lang.annotation 包中。

可以做什么

  1. 生成文档,这是最常见的,也是java 最早提供的注解。常用的有@param @return 等
  2. 跟踪代码依赖性,实现替代配置文件功能。如 @MapperScan(‘com.hc.demo’)
  3. 在编译时进行格式检查。如@override 放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出。

原理

注解本质是一个继承了Annotation 的特殊接口,其具体实现类是Java 运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java 运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler 的invoke 方法。该方法会从memberValues 这个Map 中索引出对应的值。而memberValues 的来源是Java 常量池。

元注解

java.lang.annotation 提供了四种元注解,专门注解其他的注解(在自定义注解的时候,需要使用到元注解):
@Documented – 注解是否将包含在JavaDoc中
@Retention – 什么时候使用该注解
@Target – 注解用于什么地方
@Inherited – 是否允许子类继承该注解

  1. @Retention

    • RetentionPolicy.SOURCE : 在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。@Override, @SuppressWarnings都属于这类注解。
    • RetentionPolicy.CLASS : 在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式
    • RetentionPolicy.RUNTIME : 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。我们自定义的注解通常使用这种方式。
  2. @Target

    • ElementType.CONSTRUCTOR: 用于描述构造器
    • ElementType.FIELD: 成员变量、对象、属性(包括enum实例)
    • ElementType.LOCAL_VARIABLE: 用于描述局部变量
    • ElementType.METHOD: 用于描述方法
    • ElementType.PACKAGE: 用于描述包
    • ElementType.PARAMETER: 用于描述参数
    • ElementType.TYPE: 用于描述类、接口(包括注解类型) 或enum声明
  3. @Inherited – 定义该注释和子类的关系

    @Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的annotation 类型被用于一个class,则这个annotation 将被用于该class 的子类。

常见注解

  • Override

    java.lang.Override 是一个标记类型注解,它被用作标注方法。它说明了被标注的方法重写了父类的方法,起到了断言的作用。如果我们使用了这种注解在一个没有覆盖父类方法的方法时,java 编译器将以一个编译错误来警示。就是编译的时候提醒下,编译完了就没这个注解了

    1
    2
    3
    4
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Override {
    }
  • Deprecated

    打上这个注解,编译器就会提示某个方法过期了,不建议使用,但是还是可以使用的,不一定是方法,这个可以在任意元素上加

    1
    2
    3
    4
    5
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
    public @interface Deprecated {
    }
  • SuppressWarnings

    会忽略警告,也是只存在编译期间,可以用在任意元素上

    1
    2
    3
    4
    5
    @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface SuppressWarnings {
    String[] value();
    }

自定义注解的规则

自定义注解类编写的一些规则:

  • Annotation 型定义为@interface, 所有 的Annotation 会自动继承java.lang.Annotation这一接口,并且不能再去继承别的类或是接口.
  • 参数成员只能用public 或默认(default) 这两个访问权修饰
  • 参数成员只能用基本类型byte、short、char、int、long、float、double、boolean八种基本数据类型和String、Enum、Class、annotations等数据类型,以及这一些类型的数组.
  • 要获取类方法和字段的注解信息,必须通过Java的反射技术来获取 Annotation 对象,因为你除此之外没有别的获取注解对象的方法
  • 注解也可以没有定义成员,不过这样注解就没啥用了
  • 自定义注解需要使用到元注解

自定义注解例子

https://github.com/hczs/weather-mail

自定义注解 + aop,实现自定义日志注解,在方法上加上@PrintLog注解,即可在控制台打印方法执行的日志,如方法执行时间,执行了哪个方法,方法执行完毕结束时间