面向对象编程-面向对象编程基础

1.继承--super()的注意事项

我们来看一个例子:

// super
public class Main {
    public static void main(String[] args) {
        Student s = new Student("Xiao Ming", 12, 89);
    }
}

class Person {
    protected String name;
    protected int age;

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

class Student extends Person {
    protected int score;

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

运行上面的代码,会得到一个编译错误,大意是在Student的构造方法中,无法调用Person的构造方法。

这是因为在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();所以,Student类的构造方法实际上是这样:

class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
        super(); // 自动调用父类的构造方法
        this.score = score;
    }
}

但是,Person类并没有无参数的构造方法,因此,编译失败。

解决方法是调用Person类存在的某个构造方法。例如:

class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
        super(name, age); // 调用父类的构造方法Person(String, int)
        this.score = score;
    }
}

这样就可以正常编译了!


在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。例如:

class Person {
    protected String name;
    public String hello() {
        return "Hello, " + name;
    }
}

class Student extends Person {
    @Override
    public String hello() {
        // 调用父类的hello()方法:
        return super.hello() + "!";
    }
}

2.继承-阻止继承

final修饰符有多种作用:

  • final修饰的方法可以阻止被覆写;
  • final修饰的class可以阻止被继承;
  • final修饰的field必须在创建对象时初始化,随后不可修改。

从Java 15开始,允许使用sealed修饰class,并通过permits明确写出能够从该class继承的子类名称。

例如,定义一个Shape类:

public sealed class Shape permits Rect, Circle, Triangle {
    ...
}

上述Shape类就是一个sealed类,它只允许指定的3个类继承它。如果写:

public final class Rect extends Shape {...}

是没问题的,因为Rect出现在Shapepermits列表中。但是,如果定义一个Ellipse就会报错:

public final class Ellipse extends Shape {...}
// Compile error: class is not allowed to extend sealed class: Shape

原因是Ellipse并未出现在Shapepermits列表中。这种sealed类主要用于一些框架,防止继承被滥用。

sealed类在Java 15中目前是预览状态,要启用它,必须使用参数--enable-preview--source 15

3.继承--向下转型

向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException

为了避免向下转型出错,Java提供了instanceof操作符,可以先判断一个实例究竟是不是某种类型:

Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false

Student s = new Student();
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Student); // true

Student n = null;
System.out.println(n instanceof Student); // false

instanceof实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为null,那么对任何instanceof的判断都为false

利用instanceof,在向下转型前可以先判断:

Person p = new Student();
if (p instanceof Student) {
    // 只有判断成功才会向下转型:
    Student s = (Student) p; // 一定会成功
}

从Java 14开始,判断instanceof后,可以直接转型为指定变量,避免再次强制转型。例如,对于以下代码:

Object obj = "hello";
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.toUpperCase());
}

可以改写如下:

// instanceof variable:
public class Main {
    public static void main(String[] args) {
        Object obj = "hello";
        if (obj instanceof String s) {
            // 可以直接使用变量s:
            System.out.println(s.toUpperCase());
        }
    }
}

这种使用instanceof的写法更加简洁。

4.多态-到底什么是多态?

一个实际类型为Student,引用类型为Person的变量,调用其run()方法,调用的是Person还是Studentrun()方法?

Person p = new Student();
p.run(); 

运行一下代码就可以知道,实际上调用的方法是Studentrun()方法。因此可得出结论:

Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。

这个非常重要的特性在面向对象编程中称之为多态。它的英文拼写非常复杂:Polymorphic。

假设我们编写这样一个方法:

public void runTwice(Person p) {
    p.run();
    p.run();
}

它传入的参数类型是Person,我们是无法知道传入的参数实际类型究竟是Person,还是Student,还是Person的其他子类例如Teacher,因此,也无法确定调用的是不是Person类定义的run()方法。

你无法在编译时知道实际调用哪个类的 run() 方法,只有在运行时才能确定。这就是面向对象编程中多态的强大之处。

多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。

5.多态--覆写Object方法

因为所有的class最终都继承自Object,而Object定义了几个重要的方法:

  • toString()把instance输出为String
  • equals()判断两个instance是否逻辑相等;
  • hashCode()计算一个instance的哈希值。

在必要的情况下,我们可以覆写Object的这几个方法。

重写和重载:Override和Overload不同的是,如果方法签名不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override

6.抽象类--什么是抽象类?

如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法:

class Person {
    public abstract void run();
}

把一个方法声明为abstract,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person类也无法被实例化。编译器会告诉我们,无法编译Person类,因为它包含抽象方法。

有抽象方法的类必须是抽象类。

必须把Person类本身也声明为abstract,才能正确编译它

abstract class Person {
    public abstract void run();
}

7.接口--到底什么是接口?

如果一个抽象类没有字段,所有方法全部都是抽象方法:

abstract class Person {
    public abstract void run();
    public abstract String getName();
}

就可以把该抽象类改写为接口:interface

在Java中,使用interface可以声明一个接口:

interface Person {
    void run();
    String getName();
}

所谓interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。


因为interface是一个纯抽象类,所以它不能定义实例字段。但是,interface是可以有静态字段的,并且静态字段必须为final类型

public interface Person {
    public static final int MALE = 1;
    public static final int FEMALE = 2;
}

实际上,因为interface的字段只能是public static final类型,所以我们可以把这些修饰符都去掉,上述代码可以简写为:

public interface Person {
    // 编译器会自动加上public static final:
    int MALE = 1;
    int FEMALE = 2;
}

编译器会自动把该字段变为public static final类型

8.包作用域

位于同一个包的类,可以访问包作用域的字段和方法。不用publicprotectedprivate修饰的字段和方法就是包作用域。例如,Person类定义在hello包下面:

package hello;

public class Person {
    // 包作用域:
    void hello() {
        System.out.println("Hello!");
    }
}

Main类也定义在hello包下面:

package hello;

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        p.hello(); // 可以调用,因为Main和Person在同一个包
    }
}

9.包--编译和运行

假设我们创建了如下的目录结构:

work
├── bin
└── src
    └── com
        └── itranswarp
            ├── sample
            │   └── Main.java
            └── world
                └── Person.java

其中,bin目录用于存放编译后的class文件,src目录按包结构存放Java源码,我们怎么一次性编译这些Java源码呢?

首先,确保当前目录是work目录,即存放srcbin的父目录:

$ ls
bin src

然后,编译src目录下的所有Java文件:

$ javac -d ./bin src/**/*.java

命令行-d指定输出的class文件存放bin目录,后面的参数src/**/*.java表示src目录下的所有.java文件,包括任意深度的子目录。

注意:Windows不支持**这种搜索全部子目录的做法,所以在Windows下编译必须依次列出所有.java文件:

C:\work> javac -d bin src\com\itranswarp\sample\Main.java src\com\itranswarp\world\Persion.java

使用Windows的PowerShell可以利用Get-ChildItem来列出指定目录下的所有.java文件:

PS C:\work> (Get-ChildItem -Path .\src -Recurse -Filter *.java).FullName
C:\work\src\com\itranswarp\sample\Main.java
C:\work\src\com\itranswarp\world\Person.java

因此,编译命令可写为:

PS C:\work> javac -d .\bin (Get-ChildItem -Path .\src -Recurse -Filter *.java).FullName

如果编译无误,则javac命令没有任何输出。可以在bin目录下看到如下class文件:

bin
└── com
    └── itranswarp
        ├── sample
        │   └── Main.class
        └── world
            └── Person.class

现在,我们就可以直接运行class文件了。根据当前目录的位置确定classpath,例如,当前目录仍为work,则classpath为bin或者./bin

$ java -cp bin com.itranswarp.sample.Main 
Hello, world!

10.内部类

如果一个类定义在另一个类的内部,这个类就是Inner Class:

class Outer {
    class Inner {
        // 定义了一个Inner Class
    }
}

上述定义的Outer是一个普通类,而Inner是一个Inner Class,它与普通类有个最大的不同,就是Inner Class的实例不能单独存在,必须依附于一个Outer Class的实例。示例代码如下:

// inner class
public class Main {
    public static void main(String[] args) {
        Outer outer = new Outer("Nested"); // 实例化一个Outer
        Outer.Inner inner = outer.new Inner(); // 实例化一个Inner
        inner.hello();
    }
}

class Outer {
    private String name;

    Outer(String name) {
        this.name = name;
    }

    class Inner {
        void hello() {
            System.out.println("Hello, " + Outer.this.name);
        }
    }
}

观察上述代码,要实例化一个Inner,我们必须首先创建一个Outer的实例,然后,调用Outer实例的new来创建Inner实例

Outer.Inner inner = outer.new Inner();

这是因为Inner Class除了有一个this指向它自己,还隐含地持有一个Outer Class实例,可以用Outer.this访问这个实例。所以,实例化一个Inner Class不能脱离Outer实例。

Inner Class和普通Class相比,除了能引用Outer实例外,还有一个额外的“特权”,就是可以修改Outer Class的private字段,因为Inner Class的作用域在Outer Class内部,所以能访问Outer Class的private字段和方法

在Inner Class访问Outer的属性:Outer.this.name

观察Java编译器编译后的.class文件可以发现,Outer类被编译为Outer.class,而Inner类被编译为Outer$Inner.class

11.匿名内部类

// Anonymous Class
public class Main {
    public static void main(String[] args) {
        Outer outer = new Outer("Nested");
        outer.asyncHello();
    }
}

class Outer {
    private String name;

    Outer(String name) {
        this.name = name;
    }

    void asyncHello() {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello, " + Outer.this.name);
            }
        };
        new Thread(r).start();
    }
}

观察asyncHello()方法,我们在方法内部实例化了一个RunnableRunnable本身是接口,接口是不能实例化的,所以这里实际上是定义了一个实现了Runnable接口的匿名类,并且通过new实例化该匿名类,然后转型为Runnable。在定义匿名类的时候就必须实例化它,定义匿名类的写法如下:

Runnable r = new Runnable() {
    // 实现必要的抽象方法...
};

匿名类和Inner Class一样,可以访问Outer Class的private字段和方法。之所以我们要定义匿名类,是因为在这里我们通常不关心类名,比直接定义Inner Class可以少写很多代码。

观察Java编译器编译后的.class文件可以发现,Outer类被编译为Outer.class,而匿名类被编译为Outer$1.class。如果有多个匿名类,Java编译器会将每个匿名类依次命名为Outer$1Outer$2Outer$3……


static修饰的内部类和Inner Class有很大的不同,它不再依附于Outer的实例,而是一个完全独立的类,因此无法引用Outer.this,但它可以访问Outerprivate静态字段和静态方法。


笔记所有内容摘取自廖雪峰java教程 https://liaoxuefeng.com/books/java/introduction/index.html
分类: Java-Backend 标签: Java精选

评论

暂无评论数据

暂无评论数据

目录