# 继承

# 概念

继承:基于已存在的类构造一个新类。继承已存在的类就是复用(继承)这些类的方法和域。在此基础上,还可以添加一些新的方法和域,以满足新的需求,这是 Java 程序设计中的一项核心技术

反射:反射是指在程序运行期间发现更多的类及其属性的能力

# 1. 类,超类和子类

什么情况下我们可以用到继承呢,比如普通雇员在完成本职任务之后仅领取薪水, 而经理在完成了预期的业绩之后还能得到奖金。这种情形就需要使用继承.他们两个对于领取薪水的方法实现是不一样的。

# 1.1定义子类

示例:由继承 Employee 类来定义 Manager 类的格式, 关键字 extends 表示继承

public class Manager extends Employee
{
  添加方法和域
}
1
2
3
4

Employee:可以被称为超类( superclass )、 基类( base class ) 或父类( parent class)

Manager: 称为子类(subclass、) 派生类( derived class ) 或孩子类( child class )

一般常用父类和子类来表示。

通用的方法放在父类中, 而将具有特殊用途的方法放在子类中。

# 1.2 重写

子类不但继承了父类的方法,还有属性。同时子类还可以覆盖父类的方法**(重写)。在子类覆盖父类方法的时候,如果需要调用父类的方法需要通过关键字super**进行调用。

# 1.3 子类构造器

子类构造器的特点:子类的全部构造器,都会先调用父类的构造器,再执行自己。

子类会继承父类的数据,可能还会使用父类的数据。所以,子类初始化之前,一定先要完成父类数据的初始化,原因在于,每一个子类构造方法的第一条语句默认就是super();

package cn.ensource.d14_extends_constructor;
 
class F{
    public F() {
        System.out.println("F类构造器执行了!");
    }
}
 
class Z extends F{
    public Z() {
        super();
        System.out.println("Z类无参构造器执行了!");
    }
 
    public Z(String name) {
        super();
        System.out.println("Z类有参构造器执行了!");
    }
}
 
 
public class Test {
    public static void main(String[] args) {
        // 目标:先认识子类构造器的特点,再掌握子类构造器的应用场景
        Z z1 = new Z();
        Z z2 = new Z("播妞");
    }
}

结果:F类构造器执行了!
     Z类有参构造器执行了!
     F类构造器执行了!
     Z类有参构造器执行了!
//可见,不管是子类有参构造器,还是子类无参构造器,都是先调用父类构造器,然后再执行子类构造器。
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

其实,在子类构造器中,默认会有一个super()存在,不管你调用,还是不调用,都调用这个方法。

子类构造器可以通过调用父类构造器,把对象中包含父类这部分的数据先进行初始化赋值,再回来把对象里包含子类这部分的数据也进行初始化赋值

如果子类的构造器没有显式地调用超类的构造器, 则将自动地调用超类默认(没有参数 )的构造器。 如果超类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器则 Java 编译器将报告错误

# 1.4 继承层次

继承并不仅限于一个层次,类派生出来的所有类的集合被称为继承层次

如图所示从,某个特定的类到其祖先的路径被称为该类的继承链,一个祖先类可以拥有多个子孙继承链

image-20250105163625948

Java 不支持多继承,多继承功能的实现方式是通过接口来实现的。

# 1.5 多态

在 Java 程序设计语言中,对象变量是多态的。一个 父类变量既可以引用一个父类对象,也可以引用该父类子类的任何一个对象

又因为不同子类可以对父类的方法进行不同的重写,所以父类变量的方法调用可能展现出不同的效果,所有导致了多态。

满足多态有三个条件:

  • 继承
  • 覆盖(重写)
  • 向上转型

# 1.6 理解方法调用

弄清楚如何在对象上应用方法调用非常重要。下面假设要调用 x.f(args), 隐式参数 x 声明为类 C 的一个对象。下面是调用过程的详细描述:

首先 编译器会检查对象在声明过程中具备的声明类型以及所调用的方法名 f。由于可能存在重名的情况,可能存在多个名为 f 但参数类型不同的方法。

例如,可能同时存在 f(int) 和 f(String) 这样的方法。编译器会逐一列举在类 C 中所有名为 f 的方法,还会查找超类中所有可访问的名为 f 的方法 注意:超类的私有方法除外 。这是编译器获得了所有可能被调用的备用的方法。

接下来

编译器会分析方法调用中提供的参数类型。它会匹配这些参数类型与所有候选的方法参数类型,寻找一个与提供参数类型完全匹配的方法。这一步骤被称为重载解析(overloading resolution)

如果编译器无法找到与提供参数类型完全匹配的方法,或者在进行类型转换后有多个方法与之匹配,编译器将会报告错误

到这一步,编译器已经成功 确定了需要调用的方法名和参数类型.这意味着在方法调用的过程中,编译器通过上述步骤精确地确定了应该调用的方法。这个过程确保了方法调用的准确性和可靠性。

如果存在多个名为f的方法,它们的参数类型都可以接受你提供的参数,那么编译器会选择参数类型与你提供的参数类型最接近的那个方法。例如,如果你调用f(5),并且存在一个参数类型为int的f方法和一个参数类型为double的f方法,那么编译器会选择参数类型为int的f方法,因为int类型比double类型更接近你提供的参数类型。

这个过程可能会变得复杂,因为Java允许进行类型转换。例如,int可以转换成double,Manager可以转换成Employee等。所以,如果你提供的参数类型和任何一个候选方法的参数类型都不完全匹配,编译器会尝试进行类型转换,以找到一个可以接受你提供的参数的方法。
1
2
3

方法的名称和参数列表被称为方法的签名(signature)

方法的参数列表包括参数的数量、类型和顺序。方法的签名不包括方法的返回类型和访问修饰符。
1

在Java中,方法的签名由方法的名称和参数列表组成,用于唯一标识一个方法。

public void calculateSum(int a, int b)
1

在上面的示例中,方法的名称是calculateSum,参数列表是int a和int b,因此方法的签名是calculateSum(int, int)。

f(int) 和 f(String) 是两个有着相同名字但参数不同的方法,则他们的签名是不同的。
在子类里,如果你定义了一个和超类有着相同签名的方法,那这个子类方法会“覆盖”(override)超类中同签名的方法。
1
2

返回类型不是签名的一部分。不过在覆盖一个方法时,需要保证返回类型的兼容性:—>允许子类将覆盖方法的返回类型改为原返回类型的子类型。

允许子类在覆盖(重写)父类方法时,将方法的返回类型修改为父类方法返回类型的子类型。换句话说,子类可以返回更具体的子类型,而不必仅仅返回与父类方法完全相同的类型。
1

在Java中,方法的签名由方法的名称和参数列表组成,而不包括方法的返回类型。这意味着,如果两个方法具有相同的名称和参数列表,但返回类型不同,它们的方法签名是相同的。这种情况下,编译器无法区分这两个方法。

然而,当涉及到子类覆盖(重写)父类的方法时,确实需要考虑返回类型的兼容性。尽管方法的签名相同,但返回类型必须满足协变性的原则,即子类方法的返回类型必须是父类方法返回类型的子类型。这样做是为了确保子类对象可以被正确地视为父类对象,并且在使用父类方法时能够处理返回值。如果不满足这个条件,编译器会报错,因为这可能会导致类型不匹配的错误。

例如,假设有一个父类A和一个子类B,它们分别定义了一个同名方法foo(),父类方法的返回类型是A,子类方法的返回类型是B。子类B可以重写父类A的方法,并将返回类型改为B,

(1)因为B是A的子类,所以B对象可以被视为A对象。

(2)那么当我们通过子类对象调用foo()方法时,返回的是B类型的对象。

子类型返回的是父类型的子类型。也就是说,如果在子类中重写父类的方法,并将返回类型改为父类返回类型的子类型,那么子类方法的返回值将是子类型的对象。
1
class A {
    public A foo() {
        return new A();
    }
}
class B extends A {
    @Override
    public B foo() {
        return new B();
    }
}
1
2
3
4
5
6
7
8
9
10
11

这样,当我们通过父类引用调用子类对象的foo()方法时,返回的是子类对象B。这种方式称为协变**【后面有解释】**返回类型,它允许我们在子类中返回更具体的类型,提供了更好的灵活性和可读性。

因为 B 是 A 的子类,所以 B 继承了 A 的方法。这包括方法名和参数列表。
当你在子类 B 中重写父类 A 的方法时,你可以将返回类型更改为 B。这是合法的,因为 B 是 A 的子类,所以 B 的对象可以被视为 A 的对象。
当你创建一个子类 B 的对象并调用重写后的方法时,实际上你可以将返回的 B 对象视为 A 对象的一种扩展。这是多态性的体现,你可以通过父类引用调用子类方法,而不必了解具体的子类类型。
1
2
3

具体的例子:

class Animal {
    public Animal reproduce() {
        return new Animal();
    }
}
class Dog extends Animal {
    @Override
    public Dog reproduce() {
        return new Dog();
    }
}
1
2
3
4
5
6
7
8
9
10
11

在上面的代码中,有一个父类Animal和一个子类Dog。父类Animal定义了一个方法reproduce(),返回类型为Animal。子类Dog重写了父类Animal的方法,并将返回类型改为Dog。 当我们使用子类Dog的对象调用reproduce()方法时:

Dog dog = new Dog(); // 创建一个新的 Dog 对象,赋值给 dog 变量
Dog newDog = dog.reproduce(); // 调用 dog 对象的 reproduce() 方法,返回一个新的 Dog 对象,赋值给 newDog 变量
1
2

通过子类重写父类方法,我们可以更具体地表达子类对象的行为。在这个例子中,子类Dog重写了父类Animal的reproduce()方法,并确保返回的是Dog类型的对象。这样,在使用子类Dog对象调用reproduce()方法时,我们可以直接获得一个新的Dog对象。

在这里插入图片描述

详细: 当一个父类有一个方法返回某种类型,子类可以覆盖这个方法并返回该类型的子类型。

假设有一个父类 Animal 和一个子类 Dog,并且在 Animal 类中有一个方法 makeSound(),返回类型是 String,表示动物发出的声音。在子类 Dog 中,可以覆盖 makeSound() 方法并返回 Bark 类的实例,表示狗的吠声。

class Animal {
    public String makeSound() {
        return "Some generic animal sound";
    }
}

class Dog extends Animal {
    public Bark makeSound() {
        return new Bark();
    }
}

class Bark {
    public String sound() {
        return "Woof woof!";
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

在这个例子中,子类 Dog 覆盖了父类 Animal 的 makeSound() 方法,并且返回类型从 String 改变为了 Bark 类的实例,这是父类方法返回类型的子类型。这样,你可以调用 Dog 类的 makeSound() 方法得到一个更具体的结果,而不仅仅是通用的动物声音。

“协变”(covariant)是一种类型关系 —>子类型的某些属性或方法可以比父类型更具体(特化)" “协变”(covariant)是一种类型关系,指的是子类型与父类型之间的关系,在这种关系下,子类型的某些属性或方法可以比父类型更具体(特化)。在你提到的情况中,"协变的返回类型"指的是子类方法的返回类型可以是原始方法返回类型的子类型。这意味着,子类可以返回更具体的类型,而不违反方法签名的规则。

协变返回类型主要体现在子类覆盖(override)了父类方法并改变了方法的返回类型,使其返回比父类更具体的类型。具体来说,以下部分展示了协变返回类型的使用:

class Publication {
    private String title;

    public Publication(String title) {
        this.title = title;
    }
    
    public String getTitle() {
        return title;
    }

}

class Book extends Publication {
    private String author;

    public Book(String title, String author) {
        super(title);
        this.author = author;
    }
    
    public String getAuthor() {
        return author;
    }

}

class Magazine extends Publication {
    private int issueNumber;

    public Magazine(String title, int issueNumber) {
        super(title);
        this.issueNumber = issueNumber;
    }
    
    public int getIssueNumber() {
        return issueNumber;
    }

}

class Library {
    public Publication getRecommendation() {
        // 返回一个 Publication 对象作为推荐读物
        return new Publication("Generic Recommendation");
    }
}

class BookLibrary extends Library {
    @Override
    public Book getRecommendation() {
        // 返回一个 Book 对象作为推荐读物
        return new Book("The Catcher in the Rye", "J.D. Salinger");
    }
}

class MagazineLibrary extends Library {
    @Override
    public Magazine getRecommendation() {
        // 返回一个 Magazine 对象作为推荐读物
        return new Magazine("National Geographic", 123);
    }
}
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

在这个例子中,Library 类的 getRecommendation方法返回类型是 Publication,而BookLibrary和 MagazineLibrary 分别覆盖了这个方法,并将返回类型改为更具体的 Book 和 Magazine。这种方式允许子类方法返回比父类更具体的类型,而仍然保持方法签名的一致性。

所以,协变返回类型在这里的体现就是子类方法覆盖了父类方法并返回更具体的类型,这符合"子类型与父类型之间的关系,在这种关系下,子类型的某些属性或方法可以比父类型更具体(特化)"的解释。

协变(covariance)是指子类型的返回类型可以是父类型的子类型,而逆变(contravariance)是指子类型的返回类型可以是父类型的超类型。

逆变表示一个泛型类型的参数类型在继承关系中变得更加具体(即变窄)。
	逆变通常在方法参数中使用,允许传递更通用的类型。
协变表示一个泛型类型的返回类型在继承关系中变得更加通用(即变宽)。
	协变通常在方法返回值中使用,允许返回更具体的类型。
1
2
3
4

img

  • 协变:如果类型B是类型A的子类型,那么在某些上下文中,我们可以使用类型B的对象替代类型A的对象。这通常应用于方法的返回类型。例如,如果一个方法返回一个Animal类型的对象,那么它也可以返回一个Dog类型的对象(假设Dog是Animal的子类)。

  • 逆变:如果类型B是类型A的子类型,那么在某些上下文中,我们可以使用类型A的对象替代类型B的对象。这通常应用于方法的参数类型。例如,如果一个方法接受一个Dog类型的对象作为参数,那么它也可以接受一个Animal类型的对象(假设Dog是Animal的子类)。

  • 协变和逆变的主要目的是提供更大的灵活性和类型安全。它们允许我们在保持类型安全的同时,编写更通用和可重用的代码。例如,如果我们有一个处理Animal对象的方法,通过逆变,我们可以将这个方法应用于Dog对象,而无需编写专门处理Dog对象的方法。

class Animal {
    public void eat() {
        System.out.println("Animal eats");
    }
}

class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("Dog eats");
    }
}

class AnimalHelper {
    // 协变:返回类型是协变的
    public Dog getAnimal() {
        return new Dog();
    }

    // 逆变:参数类型是逆变的
    public void feedAnimal(Animal animal) {
        animal.eat();
    }

}

public class Mains {
    public static void main(String[] args) {
        AnimalHelper helper = new AnimalHelper();

        // 协变:我们可以将Dog赋值给Animal
        Animal animal = helper.getAnimal();
    
        // 逆变:我们可以将Dog传递给接受Animal的方法
        helper.feedAnimal(new Dog());
    }

}
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

在这个例子中,AnimalHelper 类中的两个方法展示了协变和逆变的概念:

getAnimal() 方法使用协变:返回类型 Dog 是协变的。这意味着你可以将一个返回类型为 Dog 的方法赋值给类型为 Animal 的引用变量。因为 Dog 是 Animal 的子类,所以这个协变操作是安全的。

feedAnimal(Animal animal) 方法使用逆变:参数类型 Animal 是逆变的。这意味着你可以将一个类型为 Dog 的对象传递给接受 Animal 类型参数的方法。由于 Dog 是 Animal 的子类,所以逆变操作也是安全的。

在 main 方法中,你展示了如何使用这些协变和逆变的特性:

协变:你调用 helper.getAnimal() 方法并将返回值赋给一个类型为 Animal 的引用变量 animal。这是因为你可以将 Dog 类型赋值给 Animal 类型,利用了返回类型的协变特性。

逆变:你调用 helper.feedAnimal(new Dog()) 方法,将 Dog 对象传递给接受 Animal 类型参数的方法。这是因为你可以将 Dog 类型传递给接受 Animal 类型参数的方法,利用了参数类型的逆变特性。

在这里插入图片描述

注意 协变: 在协变中,我们可以将派生类(如 Dog)的实例分配给基类(如 Animal)的引用。这是因为派生类是基类的一个特殊类型,具有基类的所有属性和方法,但可能还有额外的特性。 协变涉及返回类型。例如,在协变中,你可以在方法返回类型为 Dog 的情况下,将返回值赋给类型为 Animal 的引用变量。

逆变 在逆变中,我们可以将基类(如 Animal)的实例传递给接受派生类(如 Dog)类型参数的方法。这是因为基类是派生类的通用类型,基类对象中包含了派生类对象的通用行为。 逆变涉及方法参数类型。例如,在逆变中,你可以将类型为 Dog 的参数传递给接受 Animal 类型参数的方法。

在这里插入图片描述

区别 在协变中,“赋值” 涉及将一个具体类型(如 Dog)赋值给更通用的类型(如 Animal)的引用变量。这是由于返回类型的协变特性,可以保证派生类对象的特性在基类引用中仍然有效。 在逆变中,“传递” 涉及将一个基类类型的对象传递给接受派生类类型参数的方法。这是由于参数类型的逆变特性,允许基类对象的通用属性在派生类方法中得到正确的处理。 通过你提供的代码,你确实展示了这两种情况的应用。通过 AnimalHelper 类的 getAnimal() 方法,你展示了协变,因为你可以将 Dog 类型的实例分配给类型为 Animal 的引用变量。通过 feedAnimal(Animal animal) 方法,你展示了逆变,因为你可以将 Dog 类型的对象传递给接受 Animal 类型参数的方法。这两种特性一起允许你在泛型方法中更灵活地操作不同类型的对象。

1.具体说明是怎么实现_协变 具体来说,getAnimal方法的返回类型是Dog,但在main方法中,我们将这个Dog对象赋值给了一个Animal类型的变量。这是因为Dog是Animal的子类型,所以我们可以使用Dog对象替代Animal对象。这就是协变的概念。

class AnimalHelper {
    // 协变:返回类型是协变的
    public Dog getAnimal() {
        return new Dog();
    }
}

public class Contravariance {
    public static void main(String[] args) {
        AnimalHelper helper = new AnimalHelper();

        // 协变:我们可以将Dog赋值给Animal
        Animal animal = helper.getAnimal();
        animal.eat();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在这段代码中,Dog对象被赋值给了Animal类型的变量,这是协变的一个例子。

2.具体说明是怎么实现_逆变 在AnimalHelper类的feedAnimal方法中,参数类型是Animal。这意味着你可以传递任何Animal对象或其子类的对象给这个方法。因此,你可以将Dog对象传递给这个方法,即使它期望的是一个Animal对象。这就是逆变的概念。

在main方法中,你创建了一个Dog对象,并将其传递给了feedAnimal方法。尽管feedAnimal方法期望的是一个Animal对象,但由于Dog是Animal的子类,因此你可以将Dog对象传递给它。这就是逆变的一个具体应用。

这段代码的运行结果将是输出"Dog eats",因为feedAnimal方法调用了传入对象的eat方法,而传入的对象是一个Dog对象,所以调用的是Dog类中覆写的eat方法。

静态绑定(Static Binding)和动态绑定(Dynamic Binding) 总的来说,静态绑定在编译时就确定方法调用,速度快但不够灵活,而动态绑定在运行时根据实际对象类型确定方法调用,更加灵活但速度相对较慢。在Java中,大部分方法调用使用的是动态绑定,因为它能够支持多态性和继承等特性。

  1. 静态绑定(Static Binding):编译时期确定的方法或函数【可共享的】
静态绑定是在**编译时期确定方法或函数的调用**,不需要等到运行时期才决定。
它适用于private方法、static方法、final方法和构造器的调用。
1
2

静态绑定(static binding)是指在编译时期(compile time)就能确定方法或函数的调用,不需要等到运行时期才决定。在静态绑定中,方法或函数的调用是根据引用类型(也称为编译时类型)来决定的。当编译器在编译代码时,会根据引用类型确定调用该类型定义的方法或函数。因此,静态绑定的方法或函数调用是在编译时期就确定的,不会受到运行时期对象的实际类型的影响。

  • 发生在编译时(compile time)。
  • 适用于private、static、final方法以及构造器等情况。
  • 在编译时就能确定要调用的方法,因为方法的选择是基于引用变量的声明类型。
  • 编译器可以在编译阶段直接解析方法调用,因此速度较快。

静态绑定适用于以下情况:

Private 方法:由于private方法在同一类中是不可继承的,编译器可以直接确定要用的方法。

private方法是不能被子类重写的,所以调用private方法时,编译器可以准确地知道应该调用哪个方法。
1

Static 方法:静态方法属于类而不是实例,因此不需要实例化对象就可以调用。编译器可以根据声明的类来确定要调用的静态方法。

static方法是属于类而不是对象的,所以调用static方法时,编译器可以准确地知道应该调用哪个方法。
1

Final 方法:final修饰的方法不能被子类重写,因此编译器可以准确知道要调用的是声明的类中的final方法。

final方法是不能被子类重写的,所以调用final方法时,编译器可以准确地知道应该调用哪个方法。
1

构造器:在创建对象时,编译器根据构造器的参数列表来准确地选择合适的构造器。

构造器是用于创建对象的特殊方法,调用构造器时,编译器可以准确地知道应该调用哪个构造器。
1

静态绑定的一个特点是,它在编译时就能够解析方法调用,因此执行速度较快。但是,静态绑定缺乏动态继承和多态性的特性,因为它不会考虑对象的实际类型。相比之下,动态绑定会在运行时根据实际对象类型来确定方法调用,提供了更大的灵活性和多态性。

举例代码:

class Animal {
    public static void printType() {
        System.out.println("This is an animal.");
    }
}

class Dog extends Animal {
    public static void printType() {
        System.out.println("This is a dog.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Animal();
        Animal dog = new Dog();
        Dog realUnderDogClassDog = new Dog();
        animal.printType(); // 静态绑定,输出:"This is an animal."
        dog.printType(); // 静态绑定,输出:"This is an animal.",因为静态方法不会被子类重写
        realUnderDogClassDog.printType();// 静态绑定,输出:"This is a dog."
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

由于printType()方法是静态方法,静态方法的调用是根据引用类型来确定的,不会受到运行时期对象的实际类型的影响。所以无论animal变量引用的是Animal对象还是dog变量引用的是Dog对象,调用printType()方法时都会直接调用Animal类中定义的printType()方法。因此,输出结果都是"This is an animal."。 这就是静态绑定的特点,方法调用在编译时期就已经确定,不会根据对象的实际类型来决定。

静态常量和静态绑定是两个不同的概念。 静态常量是指在编译时期就确定并且不能被修改的常量。它通常使用关键字final来声明,并且在声明时就必须初始化。静态常量在类加载时被初始化,并且可以通过类名直接访问。静态常量是类级别的,意味着它在整个类中都是共享的,所有对象共享同一个静态常量的值。 静态绑定是指在编译时期就确定方法或函数的调用。它发生在静态方法、静态变量、静态常量的访问以及使用类名访问静态成员时。静态绑定是根据引用类型来决定方法或函数的调用,不会受到对象的实际类型的影响。这是因为静态成员和静态方法是与类关联的,不依赖于对象的创建。因此,无论使用哪个对象引用去调用静态成员或静态方法,都会调用到类中定义的静态成员或静态方法。 总结起来,静态常量是在编译时期确定并且不能被修改的常量,而静态绑定是在编译时期确定方法或函数的调用。静态常量是类级别的,可以通过类名直接访问,而静态绑定是根据引用类型来决定方法或函数的调用,并且不受对象的实际类型的影响。

在Java中,static 和 final 都是关键字,用于定义变量的特性。它们有不同的含义和用途。 首先,我们分别来看看 static、final 和 static final 的含义:

static:用于表示变量或方法是类级别的,不依赖于对象的创建,可以通过类名直接访问。但是 static 并不代表不可修改,静态变量在程序运行期间是可以被修改的。

final:用于表示常量,一旦赋值后就不能再修改。可以用于变量、方法、类等地方。

static final:将 static 和 final 结合在一起,表示一个类级别的不可修改的常量。
1
2
3
4
5

现在,我们将上述概念应用于代码,并逐步思考输出结果:

public class StaticFinalExample {
    public static String staticVariable = "Static Variable";
    public final String finalVariable = "Final Variable";
    public static final String staticFinalVariable = "Static Final Variable";

    public static void main(String[] args) {
        StaticFinalExample obj1 = new StaticFinalExample();
    
        // 输出1:访问静态变量
        System.out.println("Static Variable (obj1): " + obj1.staticVariable);
        obj1.staticVariable = "hello";  // 修改静态变量的值
        System.out.println("Static Variable (obj1): " + obj1.staticVariable);
    
        // 输出2:访问 final 变量
        System.out.println("Final Variable (obj1): " + obj1.finalVariable);
        // 尝试修改 final 变量的值不会导致编译错误,但运行时会报错
        // obj1.finalVariable = "Modified Final Variable"; 
    
        // 输出3:访问 static final 变量
        System.out.println("Static Final Variable (obj1): " + obj1.staticFinalVariable);
        // 尝试修改 static final 变量的值会导致编译错误
        // obj1.staticFinalVariable = "Modified Static Final Variable"; 
    
        // 输出4:输出修改后的值
        System.out.println("Static Variable (obj1): " + obj1.staticVariable);
    }

}
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

需要注意的是,尝试修改 final 和 static final 变量的值会导致编译错误,因为它们被声明为不可修改的常量。

在这里插入图片描述

总结:

  • static 表示类级别的,可以通过类名直接访问的变量或方法。
  • final 表示常量,一旦赋值后不能再修改。
  • static final 表示类级别的不可修改的常量。
  • static 和 final 可以独立使用,也可以结合在一起使用。

在这里插入图片描述Math类中的定义:

/**

  * The {@code double} value that is closer than any other to
  * <i>e</i>, the base of the natural logarithms.
    */
     public static final double E = 2.7182818284590452354;

 /**

  * The {@code double} value that is closer than any other to
  * <i>pi</i>, the ratio of the circumference of a circle to its
  * diameter.
    */
     public static final double PI = 3.14159265358979323846;

 /**

  * Constant by which to multiply an angular value in degrees to obtain an
  * angular value in radians.
    */
     private static final double DEGREES_TO_RADIANS = 0.017453292519943295;

 /**

  * Constant by which to multiply an angular value in radians to obtain an
  * angular value in degrees.
    */
     private static final double RADIANS_TO_DEGREES = 57.29577951308232;
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

在这里插入图片描述

  1. 动态绑定(Dynamic Binding):
动态绑定(Dynamic Binding)是一种方法调用的绑定方式,它发生在运行时(runtime)。在动态绑定中,方法的选择是基于对象的实际类型,而不仅仅是引用变量的声明类型。
它适用于非私有、非静态、非final的实例方法和接口方法的调用,实现了多态性。
1
2

动态绑定(dynamic binding)是指在运行时期(runtime)根据对象的实际类型来决定方法或函数的调用。在动态绑定中,方法或函数的调用是根据实际类型(也称为运行时类型)来决定的。当程序在运行时期调用方法或函数时,会根据对象的实际类型来确定调用该类型定义的方法或函数。因此,动态绑定的方法或函数调用是在运行时期根据对象的实际类型决定的。

  • 发生在运行时(runtime)。
  • 适用于方法调用依赖于隐式参数的实际类型的情况。
  • 在运行时根据对象的实际类型来确定要调用的方法,方法选择是基于对象的实际类型。
  • 需要在每次方法调用时进行实际的查找,因此速度相对较慢,但提供了更大的灵活性和多态性。

动态绑定适用于以下情况:

  1. 调用非私有、非静态、非final的实例方法:这些方法可以被子类重写,所以在调用这些方法时,会根据对象的实际类型来确定调用哪个方法。
  2. 调用接口方法:接口方法是抽象的方法定义,实现类需要实现接口方法,因此在调用接口方法时,会根据实现类的实际类型来确定调用哪个方法。

代码举例:

动态绑定是指在运行时期根据对象的实际类型来确定方法或函数的调用。
1
class Animal {
    public void makeSound() {
        System.out.println("动物发出声音");
    }
}

class Dog extends Animal {
    public void makeSound() {
        System.out.println("狗发出汪汪声");
    }
}

class Cat extends Animal {
    public void makeSound() {
        System.out.println("猫发出喵喵声");
    }
}

public class DynamicBindingExample {
    public static void main(String[] args) {
        Animal animal1 = new Animal();
        Animal animal2 = new Dog();
        Animal animal3 = new Cat();

    animal1.makeSound(); // 输出:"动物发出声音"
    animal2.makeSound(); // 输出:"狗发出汪汪声"
    animal3.makeSound(); // 输出:"猫发出喵喵声"

}
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
我们定义了用 Animal 类和它的两个子类 Dog 和 Cat。
Animal 类中有一个 makeSound() 方法,而子类 Dog 和 Cat 分别覆盖了该方法并提供了它们自己的实现。
在 main 方法中,我们创建了一个 Animal 对象 animal1,以及两个子类对象 animal2 和 animal3。
当我们调用 `makeSound()` 方法时,由于动态绑定的作用,实际调用的方法是根据对象的实际类型来确定的。因此,
1

animal1.makeSound() 调用的是 Animal 类的 makeSound() 方法, animal2.makeSound() 调用的是 Dog 类的 makeSound() 方法, animal3.makeSound() 调用的是 Cat 类的 makeSound() 方法。

这就是动态绑定的特性,它允许程序在运行时根据对象的实际类型来确定调用的方法,而不是根据引用类型来确定。

总结: 动态绑定的特性允许子类对象在运行时根据实际类型来调用适当的方法,而不是根据编译时类型。这使得代码可以更具适应性,当引入新的子类时,无需修改现有的代码就可以调用新子类特有的方法。

为确保在子类中覆盖(重写)超类方法时,子类方法的访问权限不能低于超类方法的访问权限。如果超类方法是 public,那么子类方法也必须声明为 public。如果违反了这一规则,编译器将会报错,因为这可能导致在某些情况下无法正常访问继承的方法。

Para_1:Java虚拟机(JVM)负责在运行时解析和执行Java程序【根据编译后的字节码文件生成和维护的。】。在Java程序启动时,JVM会加载字节码文件,并将类的方法信息存储在方法区(Method Area)中。包括创建方法表【也称为虚方法表__vtable】。 方法表用于存储类的实例方法信息和调用地址,以便在运行时进行动态绑定和多态调用。因此,形成方法表是由JVM在程序运行时完成的。【方法表的结构和存储方式是由JVM定义的,因此不同的JVM实现可能会有一些细节上的差异。】

Para_2:方法表(method table)是虚拟机为每个类预先计算【存储方法信息】的数据结构,用于记录类中所有方法的签名和要调用的实际方法【它包含了类中定义的所有方法的信息,包括方法的名称、参数类型、返回类型、访问修饰符等。】。

当程序运行并且采用动态绑定调用方法时,虚拟机会根据对象的实际类型在方法表中查找对应的方法。如果找到了与实际类型对应的方法,则调用该方法;如果没有找到,则会在父类中寻找,直到找到对应的方法或者到达顶层父类为止。【方法表的主要作用是在运行时支持动态绑定和多态性。】

Para_3:在程序运行时,当调用一个方法时,JVM会根据方法表中的信息来确定要调用的具体方法。由于方法表记录了方法的实际地址或偏移量,因此可以实现多态特性,即在运行时根据对象的实际类型来确定调用哪个具体的方法。 需要注意的是:方法表是在编译时生成的,而不是在运行时动态生成的。因此,对于动态添加的方法或者通过反射机制生成的方法,方法表中是不会包含这些方法的信息的。

Java类的返回值类型可以是任何有效的数据类型,包括原始数据类型 (如int、double.char等)、对象类型(如自定义类、String等)、数组类型等。具体的返回值类型取决于方法的定义和实现。

# 1.7 阻止继承:final类和方法

当我们希望某些类不允许扩展的类,我们可以使用下面的方式final关键字对类进行声明。所有方法自动地成为 final 方法

public final class Executive extends Manager {

}
1
2
3

如果只是某一个方法不可以被子类进行覆盖,可以单独给这个方法设置final关键字

public class Employee{
   ....
    public final String getName(){
    return name;
    }  //该方法无法被子类重写
  ....
}
1
2
3
4
5
6
7

那么final修饰类和方法主要的目的是什么?确保它们不会在子类中改变语义

解释如下:

当我们预期是要调用父类当中的方法实现的时候,由于父类存在很多子类。调用方法的时候存在多态,方法的执行结果可能不是我们所期望的。如果该类的方法被final修饰了或者类被final修饰了,那么这个引用对象调用的方法一定指向的是这个父类的方法的。

# 1.8 强制类型转换

举例:

double x = 3.405;

int nx = (int) x;  //将表达式 x 的值转换成整数类型, 舍弃了小数部分
1
2
3

当然除了应用在基本数据类型当中,应用类型也存在强制类型转换

Manager boss = (Manager) staff[0]:
1

将一个子类的引用赋给一个父类变量,编译器是允许的。但将一个超类的引用赋给一个子类变量, 必须进行类型转换

注意:

1.在进行强制转换的时候,必须满足转换的类型为原本类型的子类(可相同)。

2.在进行强制转换的时候应该通过instanceof进行检查。避免转换异常

instanceof关键字用于判断一个对象是否为某个类或接口的实例

# 1.9 抽象类

在Java中,抽象类(abstract class)是面向对象编程的一个重要特性,它允许开发者定义一种模板类,该类不能被实例化,但可以包含部分实现或完全未实现的方法。抽象类通常用于定义一组相关类的通用行为,同时允许子类提供具体实现。以下是关于Java抽象类的详细描述:

  1. 定义
  • 声明:使用 abstract 关键字来声明一个抽象类。
  • 方法:抽象类可以包含普通方法(具有完整实现的方法)和抽象方法(只有声明没有实现的方法)。如果一个类包含至少一个抽象方法,则该类必须被声明为抽象类。
  • 构造函数:抽象类可以有构造函数,即使它不能直接实例化。构造函数主要用于初始化抽象类中的成员变量或供继承它的子类调用。
public abstract class Animal {
    // 普通字段
    protected String name;

    // 构造函数
    public Animal(String name) {
        this.name = name;
    }

    // 普通方法
    public void eat() {
        System.out.println(name + " is eating.");
    }

    // 抽象方法
    public abstract void makeSound();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  1. 继承与实现
  • 继承:任何非抽象类继承自抽象类时,必须实现所有抽象方法。如果子类没有实现所有的抽象方法,那么这个子类也必须被声明为抽象类。
  • 多态性:抽象类可以通过其引用指向具体的子类对象,从而利用多态性。这意味着你可以通过父类的引用来调用子类重写的方法。
public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    public void makeSound() {
        System.out.println("Dog says: Woof woof!");
    }
}

public class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }

    @Override
    public void makeSound() {
        System.out.println("Cat says: Meow meow!");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  1. 使用场景
  • 代码复用:抽象类允许你定义一些公共的行为或属性,然后让多个子类共享这些资源,而不需要重复编码。
  • 框架设计:抽象类常用于框架的设计,其中框架提供了一些基本的功能,但是某些细节需要由最终用户来定制。
  • 规范接口:通过定义抽象方法,抽象类可以强制要求所有子类实现特定的行为,确保了一定程度上的代码一致性。
  1. 注意事项
  • 不能实例化:你不能创建抽象类的对象。试图这样做会导致编译错误。
  • 抽象方法:抽象方法只能出现在抽象类中,并且不能有方法体(即没有花括号 {} 或者方法体为空)。
  • 部分实现:抽象类可以有具体实现的方法,这使得它可以为子类提供默认行为或者部分实现逻辑。
  • final 和 abstract 的冲突:由于抽象类的目的是为了被继承,所以不能将抽象类声明为 final,因为 final 类不允许被继承。

# 1.10 受保护访问(访问修饰符)

访问权限最小的是private, 但是方法标记为public

人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法或域声明为 protected

下面归纳一下 Java 用于控制可见性的 4 个访问修饰符

1 ) 仅对本类可见 private。

2 ) 对所有类可见 public

3 ) 对本包和所有子类可见 protected。

4 ) 对本包可见—默认,不需要修饰符。

# 2.Object: 所有类的超类

Object 类是 Java 中所有类的始祖,在 Java 中每个类都是由它扩展而来的,所以,熟悉这个类提供的所有服务十分重要 。但是并不需要这样写:

public class Employee extends Object{
    
}
1
2
3

如果没有明确地指出父类,Object 就被认为是这个类的父类

可以使用 Object 类型的变量引用任何类型的对象:

 Object obj = new EmployeeC'Harry Hacker", 35000) ;  
1

Object 类型的变量只能用于作为各种值的通用持有者。要想对其中的内容进行具体的操作,还需要清楚对象的原始类型,并进行相应的类型转换:

Employee e = (Employee) obj ;//只有知道原本的类型,进行类型转换才可以对对象中的内容进行操作
1

# 2.1 equals 方法

Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。

这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用,它们一定是相等的。

然而,对于多数类来说, 这种判断并没有什么意义。

例如, 采用这种方式比较两个 Person 对象是否相等就完全没有意义。往往我们判断如果他们的身份证号码一样那就是一个人,或者电话号码一样是一个人。不同的类判断是否一样的逻辑是不一样的。所以大部分情况下等会重写Object 类中的equals 方法

实例:下面使我们通过loombook自动帮我生成的类我们进行分析一下

package com.curleyg.wang.module.system.dal.dataobject.dept;

import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.curleyg.wang.framework.tenant.core.db.TenantBaseDO;
import lombok.Generated;

@TableName("system_dept")
@KeySequence("system_dept_seq")
public class DeptDO extends TenantBaseDO {
    public static final Long PARENT_ID_ROOT = 0L;
    @TableId
    private Long id;
    private String name;
    private Long parentId;
    private Integer sort;
    @TableField(
        updateStrategy = FieldStrategy.ALWAYS
    )
    private Long leaderUserId;
    private Integer status;

    //get set toString hashCode 方法 已经省略


    @Generated
    public boolean equals(Object o) {
        //首先判断地址是否相同
        if (o == this) {
            return true;
        } else if (!(o instanceof DeptDO)) {
            //判断对象是否属于DeptDO类型
            return false;
        } else {
            DeptDO other = (DeptDO)o;//进行类型转换
            if (!other.canEqual(this)) { //this是否属于DeptDO类型
                return false;
            } else if (!super.equals(o)) {//调用父类的equals方法判断 比较的也是地址
                return false;
            } else {
                //比较id属性
                Object this$id = this.getId(); 
                Object other$id = other.getId();
                if (this$id == null) {
                    if (other$id != null) {
                        return false;
                    }
                } else if (!this$id.equals(other$id)) {
                    return false;
                }
               //比较ParentId属性
                Object this$parentId = this.getParentId();
                Object other$parentId = other.getParentId();
                if (this$parentId == null) {
                    if (other$parentId != null) {
                        return false;
                    }
                } else if (!this$parentId.equals(other$parentId)) {
                    return false;
                }
               //比较Sort属性
                label71: {
                    Object this$sort = this.getSort();
                    Object other$sort = other.getSort();
                    if (this$sort == null) {
                        if (other$sort == null) {
                            break label71;
                        }
                    } else if (this$sort.equals(other$sort)) {
                        break label71;
                    }

                    return false;
                }
                 //比较LeaderUserId属性
                label64: {
                    Object this$leaderUserId = this.getLeaderUserId();
                    Object other$leaderUserId = other.getLeaderUserId();
                    if (this$leaderUserId == null) {
                        if (other$leaderUserId == null) {
                            break label64;
                        }
                    } else if (this$leaderUserId.equals(other$leaderUserId)) {
                        break label64;
                    }

                    return false;
                }
                //比较Status属性
                Object this$status = this.getStatus();
                Object other$status = other.getStatus();
                if (this$status == null) {
                    if (other$status != null) {
                        return false;
                    }
                } else if (!this$status.equals(other$status)) {
                    return false;
                }
                 //比较Name属性
                Object this$name = this.getName();
                Object other$name = other.getName();
                if (this$name == null) {
                    if (other$name != null) {
                        return false;
                    }
                } else if (!this$name.equals(other$name)) {
                    return false;
                }

                return true;
            }
        }
    }

    @Generated
    protected boolean canEqual(Object other) {
        return other instanceof DeptDO;
    }

}
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
  1. @Generated:这是一个注解,通常由IDE或代码生成工具自动添加,表明这个方法是由工具生成的,不是手动编写的。
  2. public boolean equals(Object o):定义了equals方法,接受一个Object类型的参数o,并返回一个布尔值表示两个对象是否相等。
  3. if (o == this):首先检查传入的对象o是否与当前对象是同一个引用(即它们指向内存中的同一位置)。如果是,则直接返回true,因为同一个对象自然是相等的。
  4. else if (!(o instanceof DeptDO)):如果o不是DeptDO类型的实例,则返回false。这一步确保了只有同类型的对象才会被进一步比较。
  5. DeptDO other = (DeptDO)o;:将o强制转换为DeptDO类型,并赋值给局部变量other。由于前面已经确认了oDeptDO类型,这里不会抛出异常。
  6. if (!other.canEqual(this)):调用othercanEqual方法,传入this作为参数。这是为了确保other对象愿意与this对象进行比较。如果other对象返回false,则认为两个对象不相等。
  7. else if (!super.equals(o)):调用父类的equals方法。如果父类的equals方法返回false,那么当前对象也认为是不相等的。这一步是为了兼容父类的相等性规则。
  8. 接下来的几个if语句分别比较了idparentIdsortleaderUserIdstatusname属性。这些属性是DeptDO类的关键字段,用来决定两个对象是否应该被认为是相等的。对于每个属性,代码都会先检查该属性是否为null,然后才进行比较。如果两个非null的属性值不相等,或者一个属性为null而另一个不为null,则认为两个对象不相等。
  9. return true;:如果所有关键属性都相等,最后返回true,表示两个对象相等。
  10. protected boolean canEqual(Object other):定义了一个保护级别的canEqual方法,它接受一个Object类型的参数。这个方法检查传入的对象是否是DeptDO类型的实例。如果otherDeptDO类型,则返回true,否则返回false。这个方法的存在允许子类可以覆盖equals方法,同时保持正确的相等性逻辑。

总结:

首先需要判断地址是否一致,再进行类型是否一致,一致就需要调用父类的equals方法,再然后就是判断对象中的字段值。

# 2.2 相等测试与继承

Java 语言规范要求 equals 方法具有下面的特性:

1 ) 自反性: 对于任何非空引用 x, x.equals(x) 应该返回 true

2 ) 对称性: 对于任何引用 x 和 y, 当且仅当 y.equals(x) 返回 true, x.equals(y) 也应该返回 true

3 ) 传递性: 对于任何引用 x、 y 和 z, 如果 x.equals(y) 返 N true, y.equals(z) 返回 true, x.equals(z) 也应该返回 true

4 ) 一致性: 如果 x 和 y 引用的对象没有发生变化,反复调用 x.equals(y) 应该返回同样的结果

5 ) 对于任意非空引用 x, x.equals(null) 应该返回 false

这些规则十分合乎情理,从而避免了类库实现者在数据结构中定位一个元素时还要考虑调用 x.equals(y), 还是调用 y.equals(x) 的问题

假设有一个父类Person和一个子类EmployeePerson类可能有一些基本属性,如nameage,而Employee类可能增加了一些额外的属性,如employeeId

父类 Person

public class Person {
    private String name;
    private int age;

    // 构造函数、getter和setter省略

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

子类 Employee

public class Employee extends Person {
    private int employeeId;

    // 构造函数、getter和setter省略

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Employee)) return false;
        if (!super.equals(o)) return false;
        Employee employee = (Employee) o;
        return employeeId == employee.employeeId;
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), employeeId);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

e 是一个 Person对象, m 是一个 Employee对象

我们在进行e.equals(m)判断的时候,如果在 Employee.equals 中用 instanceof 进行检测, 则返回 true。反过来m.equals(e)也会返回true.因为对称性的原则这个方法调用也需要返回true。这就使得 Employee类受到了束缚,这个类的 equals 方法必须能够用自己与任何一个 Person 对象进行比较 而不去考虑Employee添加的employeeId部分。猛然间会让人感觉instanceof 测试并不是完美无瑕

  • 如果子类能够拥有自己的相等概念,则对称性需求将强制采用 getClass 进行检测
  • 如果由超类决定相等的概念,那么就可以使用 imtanceof进行检测,这样可以在不同子类的对象之间进行相等的比较。

# 2.3 hashCode 方法

散列码(hash code) 是由对象导出的一个整型值。散列码是没有规律的。如果 x 和 y 是两个不同的对象,x.hashCode( ) 与 y.hashCode( ) 基本上不会相同

由于 hashCode 方法定义在 Object 类中, 因此每个对象都有一个默认的散列码,其值为对象的存储地址

package com.curleyg.wang.module.system.dal.dataobject.dept;

import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.curleyg.wang.framework.tenant.core.db.TenantBaseDO;
import lombok.Generated;

@TableName("system_dept")
@KeySequence("system_dept_seq")
public class DeptDO extends TenantBaseDO {
    public static final Long PARENT_ID_ROOT = 0L;
    @TableId
    private Long id;
    private String name;
    private Long parentId;
    private Integer sort;
    @TableField(
        updateStrategy = FieldStrategy.ALWAYS
    )
    private Long leaderUserId;
    private Integer status;

    @Generated
    public int hashCode() {
        int PRIME = true;
        int result = super.hashCode();
        Object $id = this.getId();
        result = result * 59 + ($id == null ? 43 : $id.hashCode());
        Object $parentId = this.getParentId();
        result = result * 59 + ($parentId == null ? 43 : $parentId.hashCode());
        Object $sort = this.getSort();
        result = result * 59 + ($sort == null ? 43 : $sort.hashCode());
        Object $leaderUserId = this.getLeaderUserId();
        result = result * 59 + ($leaderUserId == null ? 43 : $leaderUserId.hashCode());
        Object $status = this.getStatus();
        result = result * 59 + ($status == null ? 43 : $status.hashCode());
        Object $name = this.getName();
        result = result * 59 + ($name == null ? 43 : $name.hashCode());
        return result;
    }
}
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

Equals 与 hashCode 的定义必须一致:如果 x.equals(y) 返回 true, 那么 x.hashCode( ) 就必须与 y.hashCode( ) 具有相同的值。

  1. 如果两个对象的equals方法返回true,那么它们的hashCode方法必须返回相同的值。
  2. 如果两个对象的hashCode方法返回相同的值,它们的equals方法不一定返回true

# 2.4 toString 方法

用于返回表示对象值的字符串(类的名字,随后是一对方括号括起来的域值)

System.out.println(x);
1

println方法就会直接地调用 x.toString() 井打印输出得到的字符串。

Object 类定义了 toString 方法, 用来打印输出对象所属的类名和散列码

//Object源码
public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
1
2
3
4

# 2.5 其他方法

Class getClass()

返回包含对象信息的类对象。稍后会看到 Java 提供了类运行时的描述, 它的内容被封装在 Class 类中

boolean equals(Object otherObject)

比较两个对象是否相等, 如果两个对象指向同一块存储区域, 方法返回 true ; 否 则 方法返回 false。在自定义的类中, 应该覆盖这个方法

String getName()

返回这个类的名字

Class getSuperclass( )

以 Class 对象的形式返回这个类的超类信息。

# 3. 泛型数组列表

传统数组在运行时无法动态的改变数组大小.

int actualSize = . . .;
Employee[] staff = new Employee[actualSize];
1
2

但是ArrayList 的类。它使用起来有点像数组,但在添加或删除元素时, 具有自动调节数组容量的功能,而不需要为此编写任何代码。

ArrayList 是一个采用类型参数( type parameter ) 的泛型类( generic class )。 为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面, 例如,

ArrayList<Employee> staff = new ArrayList<Eniployee>(); //声明
1

如果调用 add 且内部数组已经满了,数组列表就将自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。

如果已经清楚或能够估计出数组可能存储的元素数量, 就可以在填充数组之前调用ensureCapacity方法:

staff.ensureCapacity(1OO);
//或者直接指定大小
ArrayList<Employee> staff = new ArrayListo(1OO) ;
1
2
3

这个方法调用将分配一个包含 100 个对象的内部数组。然后调用 100 次 add, 而不用重新分配空间

与为新数组分配空间有所不同

数组列表的容量与数组的大小有一个非常重要的区别。 如果为数组分配 100 个元素的存储空间,数组就有 100 个空位置可以使用。 而容量为 100 个元素的数组列表只是拥有保存 100 个元素的潜力 (实际上,重新分配空间的话, 将会超过100 ), 但是在最初,甚至完成初始化构造之后,数组列表根本就不含有任何元素。

size方法将返回数组列表中包含的实际元素数目。例如,staff.size()方法将返回 staff 数组列表的当前元素数量, 它等价于数组 a 的 a.length()

一旦能够确认数组列表的大小不再发生变化, 就可以调用 trimToSize 方法。这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。一旦整理了数组列表的大小,添加新元素就需要花时间再次移动存储块,所以应该在确认不会添加任何元素时, 再调用 trimToSize方法

# 3.1 访问数组列表元素

数组列表自动扩展容量的便利增加了访问元素语法的复杂程度 。

方式一:

Employee e = staff.get(i);  //数组列表

Employee e = a[i];  //数组
1
2
3

对数组实施插人和删除元素的操作其效率比较低,因为数组在内存中是连续存储的,如果删除其中的一个元素会导致后面的元素全部进行迁移。

void set(int index, E obj)

设置数组列表指定位置的元素值, 这个操作将覆盖这个位置的原有内容。

参数: index 位置(必须介于 0 ~ size()-1 之间)

      obj   新的值


1
2
3
4
5
6
7
8
9
E get(int index)

获得指定位置的元素值。

参数: index 获得的元素位置(必须介于 0 ~ size()-1 之间)
1
2
3
4
5
void add(int index,E obj)

 向后移动元素, 以便插入元素

参数: index 插入位置(必须介于 0size()-l 之间)

       obj 新的值
1
2
3
4
5
6
7
E removed (int index)

删除一个元素,并将后面的元素向前移动。被删除的元素由返回值返回。

参数:index 被删除的元素位置(必须介于 0size()-1 之间)
1
2
3
4
5

# 3.2 类型化与原始数组列表的兼容性

在下面这个类中我们发现update方法和find方法没有指定参数和返回参数的具体类型。

public class EmployeeDB
{
public void update(ArrayList list) { . . . }
public ArrayList find(String query) { . . . }
}

ArrayList<Employee〉staff = . . .;//其他类的数组列表也可以调用update方法
employeeDB.update(staff);

ArrayList<Employee> result = employeeDB.find(query); // 同样将一个原始 ArrayList 赋给一个类型化 ArrayList 会得到一个警告
1
2
3
4
5
6
7
8
9
10

出于安全性的问题使用类型参数来增加安全性

public class EmployeeDB
{
public void update(ArrayList<EmployeeDB> list) { . . . }
public ArrayList<EmployeeDB> find(String query) { . . . }
}
1
2
3
4
5

# 4.对象包装器与自动装箱

基本类型可以自定装箱成对象。比如Integer 类对应基本类型 int。通常, 这些类称为包装类

包装类:

Integer、Long、Float、Double、Short、Byte、Character 、Void 和 Boolean (前6个类派生于公共的超类 Number)

对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时, 对象包装器类还是 final , 因此不能定义它们的子类。

假设想定义一个整型数组列表 不允许写成 ArrayList<int>。这里就用到了 Integer 对象包装器类。 我们可以声明一个 Integer对象的数组列表
ArrayList<Integer> list = new ArrayList<>(); 
    
list.add(3);//将自动地变换成 list.add(Integer.value0f(3));这种变换被称为自动装箱。

int n = list.get(i);//int n = list.get(i).intValue();自动拆箱
1
2
3
4
5
6

# 5.参数数量可变的方法

在 Java SE 5.0 以前的版本中,每个 Java 方法都有固定数量的参数。然而,现在的版本提供了可以用可变的参数数量调用的方法(有时称为“ 变参” 方法。

举例:

public class VarargsExample {

    // 定义一个可变参数的方法
    public static int sum(int... numbers) {
        int total = 0;
        for (int number : numbers) {
            total += number;
        }
        return total;
    }

    public static void main(String[] args) {
        // 调用可变参数的方法
        System.out.println(sum(1, 2, 3)); // 输出 6
        System.out.println(sum(10, 20, 30, 40)); // 输出 100
        System.out.println(sum()); // 输出 0
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  1. 可变参数的位置:可变参数必须是方法签名中的最后一个参数。

    例如,void method(int... nums, String s) 是无效的,但 void method(String s, int... nums)`是有效的。

  2. 内部处理:可变参数在方法内部被处理为数组。例如,int... numbers在方法内部被处理为int[] numbers。

  3. 性能考虑:虽然可变参数非常方便,但在性能敏感的应用中,创建数组可能会带来一些开销。在这种情况下,可以考虑使用其他方法来传递参数。

# 6.枚举类

定义枚举类,这个声明定义的类型是一个类, 它刚好有 4 个实例

public enuni Size { 

 SMALL,

 MEDIUM,

 LARGE,

 EXTRAJARGE

 };
1
2
3
4
5
6
7
8
9
10
11

在此尽量不要构造新对象。因此, 在比较两个枚举类型的值时, 永远不需要调用 equals, 而直接使用“ = =” 就可以了

如果需要的话, 可以在枚举类型中添加一些构造器、 方法和域。 当然, 构造器只是在构造枚举常量的时候被调用

public enum Size
{
    SMALL("S"),
    MEDIUM("M"), 
    LARGE("L") ,
    EXTRA_LARGE("XL");
    
    private String abbreviation;
    
    private Size(String abbreviation) { 
        this,abbreviation = abbreviation;
    }
    
    public String getAbbreviation() { 
        return abbreviation;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

所有的枚举类型都是 Enum 类的子类。它们继承了这个类的许多方法。其中最有用的一个是 toString, 这个方法能够返回枚举常量名

例如, Size.SMALL.toString() 将返回字符串"SMALL"
1

# 常见方法

static Enum valueOf ( Class enumClass , String name )

返回指定名字、给定类的枚举常量

String toString( )

返回枚举常量名。

int ordinal ( )

返回枚举常量在 enum 声明中的位置,位置从 0 开始计数。

int compareTo( E other )

如果枚举常量出现在 Other 之前, 则返回一个负值;如果 this=other ,则返回 0; 否则,返回正值。枚举常量的出现次序在 enum 声明中给出。

# 7.反射

反射库(reflection library) 提供了一个非常丰富且精心设计的工具集, 以便编写能够动态操纵 Java 代码的程序

# 7.1 Class 类

Object 类中的 getClass() 方法将会返回一个 Class 类型的实例

因此所有的对象都可以通过getClass()获取Class类对象实例,相同类型的对象获取到的Class类实例是相同的。
1

还可以调用静态方法 forName 获得类名对应的 Class 对象

String dassName = "java.util .Random"; 

Class cl = Cl ass.forName(dassName) ;
1
2
3

还有一种方式:

Class dl = Random,class; 直接通过类名获取
1

# 7.2 捕获异常(见第六章)

# 7.3 利用反射分析类的能力 (见第一章)