# Java教程 - 7 面向对象

# 7.7 Object类

如果一个类没有继承其他的类,那么这个类就是继承自 Object 类,所以最终所有的类都会继承自 Object。

举个栗子:

class Bird { }

class Pigeon extends Bird { }

public class ExtendTest {
    public static void main(String[] args) {
        Pigeon pigeon = new Pigeon();
        System.out.println(pigeon.getClass().getSuperclass());  // class com.doubibiji.Bird

        Bird bird = new Bird();
        System.out.println(bird.getClass().getSuperclass());    // class java.lang.Object
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

可以看到 Bird 类的父类是 Object。所以最终所有的类都会继承 Object,并拥有 Object 中的方法。

Object 中我们最常用的就是: toString()equals() 方法。

# 1 toString()

toString() 方法用于返回表示对象的字符串表示形式。当我们打印对象的时候,就会将对象转换为字符串进行输出,就会调用 toString() 方法。

因为所有的类都是继承自 Object 类的,所以我们在类中不写 toString() 方法,默认是调用了 Object 类的 toString() 方法。

举个栗子:

class Student {
    public String sid;
    public String name;
    public int age;
}

public class ObjectTest {
    public static void main(String[] args) {
        Student stu = new Student();
        System.out.println(stu);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

打印对象会输出:对象的全类名(包名+类名)和对象的哈希码的无符号十六进制表示。例如:com.doubibiji.Student@d041cf

因此,如果想修改对象打印时候的显示信息,可以在自定义类中重写 toString()方法,以便更好地控制对象的字符串表示形式。

举个栗子:

class Student {
    public String sid;
    public String name;
    public int age;

    /**
     * 重写toString()方法
     */
    @Override
    public String toString() {
        return "Student{" +
                "sid=" + sid +
                ", name=" + name +
                ", age=" + age +
                "}";
    }
}

public class ObjectTest {
    public static void main(String[] args) {
        Student stu = new Student();
        stu.sid = "001";
        stu.name = "逗比";
        stu.age = 5;

        System.out.println(stu);    // 输出:Student{sid=001, name=逗比, age=5}
    }
}
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

打印对象,会调用 toString() 方法,打印 toString() 方法返回的内容。

# 2 equals()

在比较基本数据类型变量的时候,我们使用的是 == 进行比较,比较的是两个变量的值。

而对于引用类型的变量,使用 == 进行比较,比较的也是两个变量的值,但是引用类型的变量存储的值是对象在堆中的地址,所以最终比较的是两个对象的地址,但是对于属性完全相同的两个对象,使用 == 比较也是不同的。

举个栗子:

class Student {
    public String name;

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

public class ObjectTest {
    public static void main(String[] args) {
        String str1 = new String("doubibiji");
        String str2 = new String("doubibiji");
        System.out.println(str1.equals(str2));  // true

        Student stu1 = new Student("doubibiji");
        Student stu2 = new Student("doubibiji");
        System.out.println(stu1.equals(stu2));  // false
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

为什么两个相同字符串的 String 类的对象,使用 equals 比较是相等的,但是对于相同 name 属性的 Student 类的对象又是不等的呢?

因为 String 和 Student 类都继承自 Object 类,Object 类中的 equals 方法中,使用的是 == 比较两个对象,所以比较的是两个对象的地址。但是 String 类重写了 equals 方法,修改为比较内容。所以如果我们想在比较 Student 两个对象的时候,比较 name 相等就算相等,那么也需要修改 equals 方法。

class Student {
    public String name;

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

    @Override
    public boolean equals(Object obj) {
        // 如果地址相等,说明是同一个对象
        if (this == obj) {
            return true;
        }

        // 如果obj都不是Student类型,直接return
        if (!(obj instanceof Student)) {
            return false;
        }

        Student other = (Student) obj;
        if (null != name) {
            return name.equals(other.name);
        }
        else {
            return false;
        }
    }
}

/**
 * 测试类
 */
public class ObjectTest {
    public static void main(String[] args) {
        Student stu1 = new Student("doubibiji");
        Student stu2 = new Student("doubibiji");
        Student stu3 = new Student("doubi");

        System.out.println(stu1.equals(stu2));  // true
        System.out.println(stu1.equals(stu3));  // false
    }
}
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

在上面的 Student 类中重写了 equals 方法,比较两个对象的 name 属性,如果相等,两个对象使用 equals 比较就算相等。

在 IDEA 中,在类中右键选择 Generate... -> equals() and hashcode() 就可以自动生成 equals() 和 hashcode() 方法,正常情况下重写 equals() 方法一定要重写 hashcode() ,这里没涉及到其他功能,只重写了 equals() 方法。讲集合的时候再讲为什么还需要重写 hashcode() 方法。

# 7.8 包装类

Java 是面向对象语言,在 Java 中一切皆对象。

但是基本数据类型独立之外,为了使这些基本数据类型也具有对象的特性,Java引入了对应的包装类,使得基本数据类型也能像对象一样进行操作。

Java 中针对 8 中基本数据类型提供了包装类,分别是:

  1. Integer:int
  2. Byte:byte
  3. Short:short
  4. Long:long
  5. Float:float
  6. Double:double
  7. Character:char
  8. Boolean:boolean

所以这里涉及到三种数据的转换:基本数据类型,包装类、字符串之间的转换。

下面的转换以 Int 和 Integer 为例,其他的都是相似的。

# 1 基本数据类型-包装类

本来在进行数学运算的时候,只能使用基本数据类型,在将数据放入到集合(后面的章节讲)中的时候,只能使用包装类。所以要进行相互转换。

但是在 JDK1.5 后提供了自动装箱和拆箱的功能,也就是基本数据类型和包装类可以自动进行转换。

自动装箱:基本数据类型自动转换为包装类;

自动拆箱:包装类自动转换为基本数据类型。

public class WrapperTest {
    public static void main(String[] args) {

        // 基本数据类型转包装类
        // 方式1:自动装箱,推荐用法
        Integer a = 123;
        // 方式2
        Integer b = Integer.valueOf(123);
        // 方式3:过期了,不推荐使用
        Integer c = new Integer(123);

        // 包装类转基本数据类型
        // 方式1:自动拆箱,推荐
        int x = a;
        // 方式2
        int y = a.intValue();

        int d = a + b;    // 自动拆箱,包装类可以参与数学运算
        printInteger(d);   // 传递参数自动装箱
        printInt(a);       // 传递参数自动拆箱
    }

    public static void printInt(int i) {
        System.out.println(i);
    }

    public static void printInteger(Integer i) {
        System.out.println(i);
    }
}
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

因为可以自动装箱和拆箱,所以在调用方法的时候,基本数据类型和包装类的参数可以自动转换,同时包装类也可以进行数学运算了。

# 2 包装类-字符串

我们经常需要将字符串类型的数字转换为整数,例如从键盘输入的内容,所以就需要字符串和包装类的转换。

public static void main(String[] args) {
    Integer a = 123;
    // 包装类转字符串
    // 方式1
    String s1 = a.toString();
    // 方式2
    String s2 = "" + a;

    // 字符串转包装类
    Integer b = Integer.valueOf("123");
}
1
2
3
4
5
6
7
8
9
10
11

# 3 基本数据类型-字符串

和上面包装类转字符串其实是可以合并到一起的,因为包装类和字符串可以相互转换。

public static void main(String[] args) {
    // 基本数据类型转字符串
    int a = 123;
    // 方式1
    String s1 = String.valueOf(a);
    // 方式2
    String s2 = Integer.toString(a);
    // 方式3
    String s3 = "" + a;

    // 字符串转基本数据类型
    String str = "123";
    int i = Integer.parseInt(str);
    int j = Integer.valueOf(str);   // 这里转的是包装类,但是可以自动拆箱
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 4 缓存Integer

看一下下面的代码:

public class WrapperTest {
    public static void main(String[] args) {
        Integer a = 127;
        Integer b = 127;
        System.out.println(a == b);     // true

        Integer c = 128;
        Integer d = 128;
        System.out.println(c == d);     // false
    }
}
1
2
3
4
5
6
7
8
9
10
11

懵逼不懵逼?为什么呢?

在 Java 中,对于Integer类型的值,当值在-128到127之间(包括-128和127)时,Java会缓存这些对象,以避免频繁地创建和销毁小整数对象。所以在-128到127之间创建 Integer 对象,会去缓存中找对应的 Integer 对象返回,所以多次创建返回的都是相同的对象。

所以比较 Integer 对象,使用 equals() 比较。

# 7.9 final 关键字

final 表示最终的意思,final 关键字可以用来修饰类、方法、变量。

分别有什么作用呢?

# 1 修饰类

final 关键字修饰类,那么类不能被其他类继承。

final public class Student { // Studnet类无法被其他类继承
    
}
1
2
3

在 JDK 中,String类、System 等类都是 final 的,是无法被继承的。

# 2 修饰方法

final 关键字修饰方法,方法是无法被重写的。

class Bird {
    // tweet无法被子类重写
    final public void tweet() {
        System.out.println("我会叫");
    }
}
1
2
3
4
5
6

# 3 修饰变量

final 关键字修饰变量,包括全局变量,也就是类中的属性,也包括局部变量,也就是方法的形参和方法中定义的变量。


修饰类中的属性

类中的属性使用 final 修饰后,一旦初始化,就无法修改属性的值了。

举个栗子:

class Student {
    private final String name;

    public Student(String name) {
        this.name = name;
        
        //this.name = "abc"; // 报错,上面已经初始化,无法再更改值。
    }
}
1
2
3
4
5
6
7
8
9

在上面的代码中,Student 中的成员变量 name 使用 final 修饰,那么属性一旦被初始化,就无法再更改了。

对于成员变量而言,可以使用显式初始化、代码块中初始化、构造方法中初始化都可以。

对于静态变量而言,可以使用显式初始化、静态代码块中初始化。

修饰局部变量

可以修饰方法中的变量和方法的参数。

class Course {
    public String name;
}

class Student {

    /**
     * 方法一
     */
    public final void study(final int duration) {
        // duration = 123; // 错误,无法修改

        final int a = 123;
        // a = 234;        // 错误,初始化后无法修改。
    }

    /**
     * 方法二
     */
    public final void test(final Course course) {
        // course = new Course();   // 错误,无法修改变量值,所以无法修改变量的指向
        course.name = "Chinese";    // 可以,没有修改变量course的指向,只是修改变量的属性
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

这里需要注意,当修饰的变量是引用类型的时候,我们虽然无法修改变量的指向的堆中的对象,但是可以修改对象的属性。

# 4 定义全局常量

在实际的开发中,我们一般使用 static 和 final 来共同修饰全局变量,此时的变量的值是无法修改的,也就成了全局的常量。

举个例子:

class AppCostants {
  	// 定义用户名最大长度
    public static final int USERNAME_MAX_LENGTH = 16;
  	// 定义用户名最小长度
    public static final int USERNAME_MIN_LENGTH = 8;
}
1
2
3
4
5
6

定义常量的时候,常量的名称一般统一为大些,单词之间使用下划线分隔。

# 7.10 匿名子类和匿名实现类

抽象类是无法创建实例的,所以我们可以继承抽象来,通过子类来创建对象。

但是有时候创建的子类只想使用一次,不想单独定一个类,那么也可以使用抽象类来创建匿名子类。

举个栗子,首先有一个抽象类:

abstract class Bird {
    abstract public void fly();

    abstract public void tweet();
}
1
2
3
4
5

下面使用匿名子类,重写 Bird 类中的抽象方法,实现自己的逻辑:

public class TestClass {
    public static void main(String[] args) {

        Bird bird = new Bird() {
            @Override
            public void fly() {
                System.out.println("我会飞");
            }

            @Override
            public void tweet() {
                System.out.println("我会叫");
            }
        };

        bird.fly();
        bird.tweet();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

在上面的代码中,并不是创建了 Bird 类的实例,而是创建的 Bird 类的子类的实例。然后重写了 Bird 的抽象方法。


同样的方式,还可以创建接口匿名实现类:

interface Bird {
    void fly();

    void tweet();
}

public class TestClass {
    public static void main(String[] args) {

        Bird bird = new Bird() {
            @Override
            public void fly() {
                System.out.println("我会飞");
            }

            @Override
            public void tweet() {
                System.out.println("我会叫");
            }
        };

        bird.fly();
        bird.tweet();
    }
}
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

# 7.11 内部类

内部类在实际的开发中用的不多,只是在 Android 开发中用的多点。

Java 允许在一个类中再定义类,这个类内部的类就是内部类。一般情况下,当一个类只为另一个类提供服务的函数,可以将这个类定义为内部类。

在 Java 中内部类有:

  • 成员内部类(static 和 非 static 的内部类)
  • 局部内部类(方法内、代码块内、构造方法内)

因为内部类属于外部类的成员,所以和类中的属性、方法等有一些共同之处,例如可以使用 static 来修饰,可以使用权限修饰符(public、protected等)修饰。

# 1 定义内部类

举个栗子:

class Student {

    String name = "逗比";
    static int age = 12;

    public static void test1() {
        System.out.println("我要哔哔两句");
    }

    public void test2() {
        System.out.println("我爱学习");
    }


    // 静态成员内部类
    static class Brain {
        public void test1() {
            // 静态内部类无法访问外部类非静态属性和方法,因为静态内部类属于外部类的静态成员,在类加载的时候加载,此时还没有类的实例
            // System.out.println(name);   // 报错
            // test2();     // 报错

            // 可以调用外部类的静态属性和方法
            System.out.println(age);
            // 内部类和外部类方法同名,使用类名来访问
            Student.test1();
        }
    }

    // 非静态成员内部类
    class Heart {
        private String name;

        public void test2(String name) {
            System.out.println(name);   // 访问方法形参
            System.out.println(this.name);   // 访问内部属性
            System.out.println(Student.this.name);   // 访问外部属性
            // 如果属性不重名,直接使用name属性名就可以访问

            // 内部类和外部类方法同名,使用Student.this.beat() 调用外部类
            Student.this.test2();     // 合法,调用外部类成员方法

            // 可以调用外部类的静态属性和方法
            System.out.println(age);
            test1();
        }
    }

    {
        // 代码块中定义的局部内部类
        class ClassA {

        }
    }

    public Student() {

        // 构造方法中定义的局部内部类
        class ClassB {

        }
    }

    /**
     * 在局部内部类中的方法中,如果想调用该内部类所在的方法中的局部变量,那么外部类方法中的变量需要声明为final的
     */
    public void test() {
        int flag = 10;

        // 方法中定义的局部内部类
        class ClassC {
            public void test2() {
                System.out.println(flag);   // 可以访问,jdk8不用显式声明为final,之前的需要显式声明
                // flag = 20;  // 非法,final的,无法修改
                
                System.out.println(name);   // 合法,调用外部类成员属性
                // 内部类和外部类方法同名,使用Student.this.beat() 调用外部类
                Student.this.test2();     // 合法,调用外部类成员方法

                // 可以调用外部类的静态属性和方法
                System.out.println(age);
                test1();
            }
        }
    }
}
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

在上面的代码中,演示了各种内部类的定义,以及在内部类中访问外部类成员的方式。

在实际的开发中,局部内部类一般是在方法中返回一个内部类的对象,例如下面演示了实现了 Comparable 接口的内部类,并创建对象返回。

// 定义方法
public Comparable test() {
    // 定义内部类
    class MyComparable implements Comparable {

        @Override
        public int compareTo(Object o) {
            return 0;
        }
    }

    // 返回内部类对象
    return new MyComparable();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这种方式和使用接口的匿名实现类基本是一样的:

public Comparable test() {
    return new Comparable() {
        @Override
        public int compareTo(Object o) {
            return 0;
        }
    };
}
1
2
3
4
5
6
7
8

# 2 实例化内部类

如果是在外部类中实例化内部类,直接 new 就可以了。

如果是在外部类的外面实例化内部类,就需要使用如下方式:

public class ObjectTest {
    public static void main(String[] args) {
        // 创建静态内部类实例
        Student.Brain brain = new Student.Brain();
        brain.test1();

        // 创建非静态内部类实例,首先得有外部类的对象
        Student stu = new Student();
        Student.Heart heart = stu.new Heart();
        heart.test2("zhangsan");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12