跳到内容
Caiden's Blog
返回

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

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

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

缓存池

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

例子:

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源码:

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是多少呢?

image-20220220163723386

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

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

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

String

概述

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

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

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

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

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要是可变的,那这个就不能用了,因为你正在引用的对象竟然可以在你不知道的情况下被更改…

    image-20220220173529345

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

    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的引用

例子:

String a = "hello";
String b = "hello";
String c = new String("hello");
System.out.println(a == b); // true
System.out.println(a == c); // false

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

image-20220220181401271

例子:

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

image-20220220205051843

// 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

image-20220220205235617

// 如果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类型

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加花括号,来声明一个静态代码块,这个只在类初始化的时候运行一次

    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. 静态内部类

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

    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();

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

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

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

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

    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通用方法

概览

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. 等价关系

    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判断对象是否等价
    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对象进行转型(上一步已经检查过是否是同一个类型了,所以这里直接转型没问题)
    • 判断每个关键域是否相等
    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()

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

也可以:

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

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

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + x;
    result = 31 * result + y;
    result = 31 * result + z;
    return result;
}

toString()

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

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文档:

image-20220223171325738

clone()方法可以保证:

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

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

浅拷贝

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

例子:

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;
    }
}
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();
    }
}

测试:

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一样不:

false
true
true
true

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

深拷贝

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

@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方法:

false
false
false
false

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

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

使用

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

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

public Teacher(Teacher original) {
    this.name = new String(original.name);
    this.subject = new String(original.subject);
    this.classPresident = new Student(original.classPresident.getName());
}

测试:

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());

结果:

false
false
false
false

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

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

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;
}

测试:

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());

结果:

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

继承

访问修饰符

特性

抽象类和接口

  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

两种用途

重写和重载

  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中,来连接数据库

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

    image-20220227173047677

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

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

汇总

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)方法,方法签名如下

image-20220227174558415

例子:

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

使用Class对象创建实例

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

// 使用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();
}

获取某个类的所有方法

获取类字段信息

调用方法

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

// 使用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();
}

优缺点

优点:

缺点:

异常

概述

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

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

image-20220302150727651

嵌套

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

finally

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

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

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

例子:

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

引用类型:

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、?

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才是泛型方法

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

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

说明:

使用

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

泛型方法与可变参数

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的子类才行

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

如果不是,编译会报错

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就不行,限定了下界

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中不允许创建确切类型的泛型数组

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.
}

除非是采用通配符的方式

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 的子类。

常见注解

自定义注解的规则

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

自定义注解例子

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

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


分享到:

上一篇
模拟并发环境代码段
下一篇
代理模式(Proxy)