封装、继承、多态

2023/12/20 后端Java基础

首先是访问修饰符,public、private和protected,其实之前在学校学习c++和java时已经都学过了,一些比较常见简单的不再记录,直接记录多态。 后面顺便记录了 Object 类的一些常用方法。

先看几个类:

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

    public Person() {

    }

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void playWithAnimal(Animal animal) {
        System.out.println("和" + animal.getName() + "玩耍");
        // 因为运行时类型是子类,所以调用的是子类的方法
        animal.bark();
    }

    public void sayHello() {
        System.out.println("Hello, I'm " + name + " and I'm " + age + " years old.");
    }


    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

public class Student extends Person{
    private int id;
    private int score;

    Student(int id, int score) {
        // super(); // 即使没写也会默认调用父类的构造方法
        this.id = id;
        this.score = 0;
    }

    Student(String name, int age, int id, int score) {
        super(name, age);
        // 注意,this()和super()的顺序不能同时出现,因为构造方法中默认会调用super
        // this(id, score);
        this.id = id;
        this.score = score;
    }



    public void sayHello() {
        super.sayHello();
        System.out.println("And my id is " + id + " and my score is " + score);
    }

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public int getScore() {
        return score;
    }
    public void setScore(int score) {
        this.score = score;
    }
}

public class Animal {
    private String name;
    private int age;
    public String type = "animal";

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void printType() {
        System.out.println(type);
    }

    public String getType(){
        return type;
    }

    public void bark() {
        System.out.println("Animal " + this.name + " is barking");
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }


    @Override
    public String toString() {
        return "Animal{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", type='" + type + '\'' +
                '}';
    }
}

public class Cat extends Animal {
    public String type = "Cat";

    Cat(String name, int age) {
        super(name, age);
    }

    public void printType(){
        System.out.println(this.type);
    }

    public void bark() {
        System.out.println("meow");
    }

    // 特有方法 抓老鼠
    public void catchMouse() {
        System.out.println("catch mouse");
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
}

public class Dog extends Animal{
    public String type = "Dog";

    Dog(String name, int age){
        super(name, age);
    }

    public void bark(){
        System.out.println("wang wang");
    }

    public void printType(){
        System.out.println(type);
    }

    public String getType(){
        return type;
    }

    // 特有方法 看门
    public void lookAfterHouse(){
        System.out.println("look after the house");
    }
}

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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183

然后直接上代码实践:

package com.azusa.practise;

public class TestObject {
    public static void main(String[] args) {
        // 继承、重写    重载是针对同个类中的同名方法,但参数个数、类型、顺序不同,重写是针对父子类的,子类重写父类的方法。
        // Person person = new Person("张三", 20);
        // Student student = new Student("李四", 20, 10000, 20);
        // person.sayHello();
        // student.sayHello();

        // 多态  编译类型是父类,运行类型是子类,运行类型可变,编译类型不变。
        // 父类的引用指向子类对象,可以调用子类的方法,称为向上转型。
        Animal dog = new Dog("十七", 5);
        Animal cat = new Cat("妙妙", 3);

        Person person = new Person("张三", 20);
        person.playWithAnimal(dog); // wangwang
        person.playWithAnimal(cat); // meow


        // 不能调用子类特有的方法,因为编译时类型是父类,识别不了子类特有的方法。
        // cat.catchedMouse();
        // 在调用时,会从运行类型(也就是子类)中寻找方法,如果找到,则调用。
        // 如果没有找到,则一层一层向父类寻找方法,直到找到或到达最顶层的父类,如果还是没有找到,则抛出异常。


        // 向下转型,将向上转型后的对象,强制转换为 对应 子类类型,可以调用子类特有的方法,称为向下转型。
        Cat cat1 = (Cat) cat;
        // 此时就能够调用 Cat 特有的方法了。
        cat1.catchMouse(); // catch mouse
        // 但是强制转换的类型 必须和对象运行的类型一致,否则会报错。例如 Cat cat1 = (Cat) dog 则是错误的。

        // 属性和方法不同,属性的值在编译时就已经确定了,方法在运行时才确定。
        System.out.println(dog.type); // 输出的是 animal
        dog.printType(); // 这里输出的是 Dog,因为方法看的是运行类型,虽然打印的是 this.type,但还是指向子类对象。
        // 如果子类没有 printType() 方法,并且父类中获取type调用的是getType()方法,那么由于动态绑定机制
        // 父类调用 printType() 方法中 的 getType() 方法其实是子类的,所以打印的是子类的 type。

        // 另外注意 instanceof 这个关键字,它是用来判断一个对象是否是某个类或者其子类的实例。
        // 但是在多态中可能会比较模糊,实际上判断的都是运行时的类型
        System.out.println(dog instanceof Dog); // true
        System.out.println(dog instanceof Animal); // true
        System.out.println(cat1 instanceof Cat); // true
        System.out.println(cat1 instanceof Animal); // true

        // 多态数组  多态数组的元素类型是父类,但是元素的运行类型可以是不同的子类。
        Animal[] animals = new Animal[3];
        animals[0] = new Animal("小花", 1); // 向上转型
        animals[1] = new Dog("小狗", 2);
        animals[2] = new Cat("小猫", 3);
        for (Animal animal : animals) {
            animal.bark();
            // 如果要调用子类特有的方法,需要先判断类型后,向下转型
            if (animal instanceof Dog) {
                ((Dog)dog).lookAfterHouse();
            } else if (animal instanceof Cat) {
                ((Cat)cat).catchMouse();
            }
        }


        // Object常用方法
        // equals

        // == 可以判断基本类型的值是否相等,也可以判断引用类型的地址是否相等,也就是是否是同一个对象。
        int x = 10;
        double y = 10.0;
        System.out.println(x == y); // true

        // 引用类型,虽然编译类型不一致,但引用都是指向同一个对象,所以是相等的。
        B b = new B();
        A a = b;
        System.out.println(a == b); // true

        // 与 == 不同,equals 是Object类的方法,只能判断对象引用是否相等,不能判断对象的值是否相等。
        // 但是 Object 的子类往往重写了 equals 方法,可以判断对象的值是否相等。
        // 例如 String 类
        String s1 = new String("hello");
        System.out.println(s1 == "hello"); // false
        System.out.println("hello".equals(s1)); // true
        // 注意子类的equals方法中,形参都是 Object 类型,但传入的类型可以是子类,这里体现了参数多态。

        /*
        String a = "hello"; 和 String a = new String("hello"); 在语义上是相似的,它们都创建了一个包含字符串"hello"的String对象。
        然而,这两种方式在内存中的行为有所不同:
        "hello" 是一个字符串字面量(String literal),它在编译时就被放入常量池中。当使用String a = "hello";时,Java虚拟机会首先检查常量池中是否已经存在该字符串,如果存在,则直接返回对常量池中字符串的引用;如果不存在,才会在常量池中创建一个新的字符串对象。因此,在代码中多次出现相同的字符串字面量时,实际上使用的是同一个字符串对象。
        new String("hello") 则是使用构造函数创建一个新的String对象。无论常量池中是否存在相同内容的字符串,都会创建一个新的对象。这意味着即使有一个相同内容的字符串已经存在于常量池中,仍然会创建一个新的String对象,占用额外的内存空间。
        总结起来,使用字符串字面量创建String对象具有更好的性能和内存效率,因为它可以利用常量池的重用特性。而使用new String()构造函数则会始终创建一个新的字符串对象,不论常量池中是否已经存在相同的字符串。
        需要注意的是,当进行字符串拼接或进行其他操作时,使用字符串字面量或new String()创建的String对象并没有本质的区别,它们的行为是一致的。
         */

        /*
        注意,不同类型的基本类型之间可以用 == 去比较,但是不同类型的引用类型之间不能用 == 去比较。
        最好是使用 equals 方法来判断两个引用类型的值是否相等。
        如果对象之间使用 == 来比较时,不是相同类型或者存在继承关系,则会报错。如:
        System.out.println(new Integer(10) == new String("10"));
        而使用 equals 方法比较时,如果两个对象类型不同,则不会报错,而是返回 false。如:
        System.out.println(new Integer(10).equals(new String("10")));
         */

        // hashCode
        /*
        1)提高具有哈希结构的容器的效率!
        2)两个引用,如果指向的是同一个对象,则哈希值肯定是一样的!
        3)两个引用,如果指向的是不同对象,则哈希值是不一样的(一般来说是不一样的,但有极小的概率会冲突)
        4)哈希值主要根据地址号来的!, 不能完全将哈希值等价于地址。
        5)后面在集合,中hashCode 如果需要的话,也会重写
         */

        Object o = new Object();
        Object o1 = new Object();
        Object o2 = o;
        // o 和 o2 的hashcode相等,与o1不相等
        System.out.println("o的hashcode" + o.hashCode() + ", o1的hashcode" + o1.hashCode() + ", o2的hashcode" + o2.hashCode());


        // toString
        // 默认返回全类名(包名+类名) + @ + 16进制的的hashCode
        System.out.println(o.toString()); // java.lang.Object@14ae5a5
        System.out.println("十进制的hashCode:" + o.hashCode() + ",16进制的hashCode:" + Integer.toHexString(o.hashCode()));
        // 十进制的hashCode:21685669,16进制的hashCode:14ae5a5

        // 一般子类也都会重写 toString 方法,可以返回自定义的字符串。
        System.out.println(new Animal("绿茶", 3).toString()); // 调用了 Animal 类中的 toString 方法。
        System.out.println(new Animal("绿茶", 3)); // 当直接输出一个对象时,会默认调用 toString 方法。
        System.out.println(new Integer(100).toString()); // 100 说明Integer 类重写了 toString 方法,只输出具体值。

        
        // finalize
        /*
        垃圾回收器会调用finalize方法来释放对象占用的内存。
        一般情况下,我们不需要手动调用finalize方法,但是如果我们创建的对象持有资源,需要在垃圾回收器调用finalize方法时释放资源。
        例如,数据库连接,文件句柄,网络连接等。
        我们也可以对finalize方法进行重写,在finalize方法中释放资源。
        但该方法在java9后就废弃了,了解即可。
        */
        o2 = null;
        // 当时机合适时,垃圾回收器会调用finalize方法来释放对象占用的内存。
        // 或者,我们可以手动调用System.gc()来强制垃圾回收器立即回收对象占用的内存。

    }
}

class A {}
class B extends A {}
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
Last Updated: 2024/9/4 15:42:44