已复制
全屏展示
复制代码

Java 面向对象知识点梳理


· 20 min read

一. 类和实例

  • 一个 .java 文件只能包含一个public类,但可以包含多个非public类。如果有public类,文件名必须和public类的名字相同。
  • 如果不确定是否需要public,就不声明为public,即尽可能少地暴露对外的字段和方法。
  • 创建新对象时,优先选用静态工厂方法而不是new操作符。
  • 定义为public的class、interface,可以被当前包,其他包的任何类访问。
class Person {
    public String name;
    public int age;
}

Person ming = new Person();
ming.name = "Xiao Ming";
ming.age = 12;
System.out.println(ming.name);

1.1 类加载机制

类并不是在一开始就全部加载好,而是在需要时才会去加载(提升速度)以下情况会加载类:

  • 访问类的静态变量,或者为静态变量赋值
  • new 创建类的实例(隐式加载)
  • 调用类的静态方法
  • 子类初始化时

所有被标记为静态的内容,会在类刚加载的时候就分配,而不是在对象创建的时候分配,所以说静态内容一定会在第一个对象初始化之前完成加载。

public class Student {
    // 直接调用静态方法,只能调用静态方法
    static int a = test();

    Student(){
        System.out.println("构造类对象");
    }

    // 静态方法刚加载时就有了
    static int test(){
        System.out.println("初始化变量a");
        return 1;
    }
}
  • 思考下面情况
public class Student {
    static int a = test();

    static int test(){
        return a;
    }

    public static void main(String[] args) {
        System.out.println(Student.a);
    }
}

定义和赋值是两个阶段,在定义时会使用默认值(上面讲的,类的成员变量会有默认值)定义出来之后,如果发现有赋值语句,再进行赋值,而这时,调用了静态方法,所以说会先去加载静态方法,静态方法调用时拿到a,而a这时仅仅是刚定义,所以说还是初始值,最后得到0

二. 方法和属性

2.1 public private protected

方法和属性都可以用public private protected修饰

public

可以通过实例访问。

private

不能通过实例访问,可以在类内部访问,子类不能访问。

  • private访问权限被限定在class的内部。推荐把private修饰属性和方法放到后面,因为public定义了类对外提供的功能,应该先关注public。
  • 由于Java支持嵌套类,如果一个类内部还定义了嵌套类,那么,嵌套类拥有访问private的权限。
protected

不能通过实例访问,可以在类内部访问,子类可以访问,子类的子类可以访问。

2.2 this变量

在方法内部,可以使用一个隐含的变量this,它始终指向当前实例。因此,通过this.field就可以访问当前实例的字段。如果没有命名冲突,可以省略this

2.3 方法可变参数

方法可以包含0个或任意个参数,调用方法时,必须严格按照参数的定义(类型、位置)一一传递。

  • 定义:可变参数用 类型... 定义,可变参数相当于数组类型。
class Group {
    private String[] names;
    public void setNames(String... names) {
        this.names = names;
    }
}

Group g = new Group();
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun");   // 传入3个String
g.setNames("Xiao Ming", "Xiao Hong");               // 传入2个String
g.setNames("Xiao Ming");                            // 传入1个String
g.setNames();                                       // 传入0个String,此时names是一个空数组
  • 等价用法:完全可以把可变参数改写为String[]类型,但是,调用方需要自己先构造String[],比较麻烦。
class Group {
    private String[] names;
    public void setNames(String[] names) {
        this.names = names;
    }
}

Group g = new Group();
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"});   // 传入1个String[]

2.4 参数传递

调用方法时,传递的参数方式有两种:

  • 值传递:基本数据类型的传递都是值传递,在方法内部修改传递的参数不会影响调用处传递的参数值。
  • 引用传递:在方法内和方法外处理的是同一个对象。

2.5 构造方法

  • 基本用法:创建实例时会自动调用该方法,通常用于初始化实例的字段。构造方法的名称就是类名,在方法内部,也可以编写任意语句,它也没有返回值。
public class Main {
    public static void main(String[] args) {
        Person p = new Person("Xiao Ming", 15);
    }
}

class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
  • 字段默认值:在构造方法内没有编写初始化语句时,引用类型的字段默认是null,数值类型的字段用默认值,int类型默认值是0,布尔类型默认值是false。
  • 默认构造方法:任何类都有构造方法,如果不编写构造方法,编译器会自动创建一个构造方法
class Person {
    public Person() {
    }
}
  • 初始化顺序:既在类里面对字段进行初始化,又在构造方法中对字段进行初始化时,先初始化字段,例如int age = 10; 然后执行构造方法的代码进行初始化。
class Person {
    private String name = "Unamed";
    private int age = 10;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
  • 多构造方法:当初始化实例时需要多种形式的初始化,可以使用多个构造方法,初始化时会根据参数的类型和个数来识别具体调用的是哪个构造方法。
class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public Person(String name) {
        this(name, 18);                    // 调用另一个构造方法Person(String, int)
    }
    public Person() {
        this("Unnamed");                   // 调用另一个构造方法Person(String)
    }
}

new Person("Xiao Ming", 20);               // 自动匹配到构造方法public Person(String, int)
new Person("Xiao Ming");                   // 自动匹配到构造方法public Person(String)
new Person();                              // 自动匹配到构造方法public Person()

2.6 方法重载

  • 在一个类中,如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。例如,在Hello类中,定义多个hello()方法
class Hello {
    public void hello() {
        System.out.println("Hello, world!");
    }

    public void hello(String name) {
        System.out.println("Hello, " + name + "!");
    }

    public void hello(String name, int age) {
        if (age < 18) {
            System.out.println("Hi, " + name + "!");
        } else {
            System.out.println("Hello, " + name + "!");
        }
    }
}
  • 方法重载是为了功能统一,返回类型通常都是相同的(也可以是不同的)。
  • 多个构造方法也可以看成是方法重载。

2.7 静态字段和静态方法

  • 在一个class中定义的字段,我们称之为实例字段。实例字段的特点是,每个实例都有独立的字段,各个实例的同名字段互不影响。还有一种字段,是用static修饰的字段,称为静态字段。实例字段在每个实例中都有自己的一个独立空间,但是静态字段只有一个共享空间,所有实例都会共享该字段。用static修饰的方法称为静态方法。
public class Main {
    public static void main(String[] args) {
        Student.setSchool("beijing university");
        System.out.println(Student.school);
    }
}

class Student {
    String name;
    int age;
    static String school;
    public static void setSchool(String value) {
        school = value;
    }
}
  • 静态字段和方法不推荐使用实例去访问,因为它不属于实例,而是用类名访问。
  • 因为静态方法属于class而不属于实例,因此,静态方法内部,无法访问this变量,也无法访问实例字段,它只能访问静态字段。
  • 通过实例变量也可以调用静态方法,但这只是编译器自动帮我们把实例改写成类名而已。
  • 静态方法经常用于工具类。例如:Arrays.sort(); Math.random(); 静态方法也经常用于辅助方法。注意到Java程序的入口 main() 也是静态方法。

2.8 代码块和静态代码块

  • 静态代码块只加载一次,和静态变量类似
  • 代码块每次 new 对象都会调用,如果有多个构造方法的话,相当于把逻辑抽取出来了。
package org.example;

public class Student {
    static {
        System.out.println("我是静态代码块");
    }

    {
        System.out.println("我是代码块");
    }

    Student() {
        System.out.println("我是构造方法 1");
    }
    Student(String name) {
        System.out.println("我是构造方法 2");
    }

    public static void main(String[] args) {
        new Student();
        new Student("zhang");
    }
}

三. 继承

3.1 基本用法

  • 继承可以复用代码,可以在已有类的基础上增加新的功能。Java 使用 extends 关键字来实现继承。任何类,除了 Object,都会继承自某个类。定义类时,如果没有写extends,编译器会自动加上extends Object。Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object特殊,它没有父类。
class Person {
    private String name;
    private int age;

    public String getName() {...}
    public void setName(String name) {...}
    public int getAge() {...}
    public void setAge(int age) {...}
}

class Student extends Person {
    private int score;

    public int getScore() { … }
    public void setScore(int score) { … }
}

3.2 protected

  • 子类无法访问父类的private修饰的字段或方法。例如,Student类就无法访问Person类的name和age字段。所以出来了protected,一个protected字段和方法可以被其子类,以及子类的子类访问。

3.3 父类super

  • super关键字表示父类(基类)。子类引用父类的字段时,可以用super.fieldName。
  • 在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super()。如果父类有自定义的构造方法,子类就必须显式调用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;
    }
    
    public String hello() {
        return "Hello, " + name;
    }
}

class Student extends Person {
    protected int score;
    public Student(String name, int age, int score) {
        super(name, age);
        this.score = score;
    }
    
    @Override
    public String hello() {
        // 调用父类的hello()方法:
        return super.hello() + "!";
    }
}

3.4 类型转换

  • 向上转型
    如果一个引用变量的类型是Student,那么它可以指向一个Student类型的实例,比如:Student s = new Student();  如果一个引用类型的变量是Person,那么它可以指向一个Person类型的实例,比如:Person p = new Person(); 如果Student是从Person继承下来的,那么,一个引用类型为Person的变量,可以指向Student类型的实例,这是因为Student继承自Person,因此,它拥有Person的全部功能。这种把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。
Student s = new Student();
Person p = s;               // upcasting, ok
Object o1 = p;              // upcasting, ok
Object o2 = s;              // upcasting, ok
  • 向下转型
    和向上转型相反,如果把一个父类类型强制转型为子类类型,就是向下转型(downcasting)。Person类型p1实际指向Student实例,Person类型变量p2实际指向Person实例。在向下转型的时候,把p1转型为Student会成功,因为p1确实指向Student实例,把p2转型为Student会失败,因为p2的实际类型是Person,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。
Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!
  • 为了避免向下转型出错,Java提供了instanceof操作符,可以先判断一个实例究竟是不是某种类型:
Person p = new Person();
if (p instanceof Student) {
    Student s = (Student) p;
}

3.5 覆写Override

  • 常用方法
    在继承关系中,子类如果定义了一个与父类方法签名完全相同(包括:方法名、方法参数、返回值)的方法,被称为覆写(Override)。加上@Override可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。但是@Override不是必需的。
class Person {
    public void run() {
        System.out.println("Person.run");
    }
}
class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}
  • 覆写Object的方法,有时需要覆写一些必要的方法,这些方法可以简化一些功能,比如:
  • toString():把instance输出为String;
  • equals():判断两个instance是否逻辑相等;
  • hashCode():计算一个instance的哈希值;
class Person {
    @Override
    public String toString() {
        return "Person:name=" + name;
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof Person) {
            Person p = (Person) o;
            return this.name.equals(p.name);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return this.name.hashCode();
    }
}

3.6 多态

  • 多态定义
    多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。在上一节中已经知道,引用变量的声明类型可能与其实际类型不符,例如:Person p = new Student(); 那么此时如果Student类如果覆写了run方法,此时Person p = new Student(); p.run(); 调用的是哪个类的方法呢,答案是Student类的。
  • 使用方法
    假如编写下面这样一个方法,它传入的参数类型是Person,我们是无法知道传入的参数实际类型究竟是Person,还是Student,还是Person的其他子类,因此,也无法确定调用的是不是Person类定义的run()方法。所以,多态的特性就是,运行期才能动态决定调用的子类方法。对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法。
public void runTwice(Person p) {
    p.run();
    p.run();
}
  • 使用场景
    假如每个人的收入分为两部分:工资收入、额外收入,想要计算每个人的税额,首先定义一个基类,再定义工资税类、额外收入类分别都继承自基类,覆写获取税的方法,详细内容看如下代码。如果以后新增了一种收入,只需要编写一个继承自基类的类,覆写 getTax 方法即可。
public class Main {
    public static void main(String[] args) {
        Income[] incomes = new Income[] {
                new SalaryIncome(7000),
                new ExtraIncome(15000)
        };
        System.out.println(totalTax(incomes));
    }
    private static double totalTax(Income... incomes) {
        double total = 0;
        for (Income income: incomes) {
            total = total + income.getTax();
        }
        return total;
    }
}


class Income {
    protected double income;
    public Income(double income) {
        this.income = income;
    }
    public double getTax() {
        return 0;
    }
}


class SalaryIncome extends Income {
    public SalaryIncome(double income) {
        super(income);
    }
    @Override
    public double getTax() {
        if (income <= 5000) {
            return 0;
        }
        return (income - 5000) * 0.2;
    }
}


class ExtraIncome extends Income {
    public ExtraIncome(double income) {
        super(income);
    }
    @Override
    public double getTax() {
        return this.income * 0.1;
    }
}

3.7 final修饰

  • 用final修饰的类不能被继承: final class Person {}。
  • 用final修饰的属性在初始化后不能被修改: public final String name = "Unamed"; 可以在构造方法中初始化final字段。
  • 用final修饰的方法不能被覆写: public final String hello() {}。
  • 用final修饰局部变量可以阻止被重新赋值:protected void hi(final int age)。

3.8 抽象类

  • 如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法,因为无法执行抽象方法,因此这个类也必须申明为抽象类(abstract class)。抽象方法实际上相当于定义了“规范”。
abstract class Person {
    public abstract void run();
}

class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}
  • 抽象类需要注意:抽象类不允许实例化、抽象类里面除了抽象方法外,也可以存在非抽象方法、正常的属性字段。
  • 面向抽象编程
    当我们定义了抽象类Person,以及具体的Student、Teacher子类的时候,我们可以通过抽象类Person类型去引用具体的子类的实例,这种引用抽象类的好处在于,我们对其进行方法调用,并不关心Person类型变量的具体子类型
Person s = new Student();
Person t = new Teacher();
s.run();
t.run();

// 同样不关心新的子类是如何实现run()方法的:
Person e = new Employee();
e.run();

面向抽象编程本质:

  • 上层代码只定义规范(方法签名);
  • 不需要子类就可以实现业务逻辑(正常编译);
  • 具体的业务逻辑由不同的子类实现,调用者并不关心(调用抽象类定义的方法名)。

3.9 接口

  • 如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口。在Java中,使用interface可以声明一个接口。所谓interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。当一个具体的class去实现一个interface时,需要使用implements关键字。
interface Person {
    void run();
    String getName();
}

class Student implements Person {
    private String name;

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

    @Override
    public void run() {
        System.out.println(this.name + " run");
    }

    @Override
    public String getName() {
        return this.name;
    }
}
  • 接口的实现
    在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface,下面示例表示实现两个接口。
class Student implements Person, Hello {
    ...
}
  • 接口继承
    一个interface可以继承自另一个interface。interface继承自interface使用extends,它相当于扩展了接口的方法。例如:
interface Hello {
    void hello();
}

interface Person extends Hello {
    void run();
    String getName();
}
  • 接口与实现类的通常关系
    在使用的时候,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象
List list = new ArrayList();    // 用List接口引用具体子类的实例
Collection coll = list;         // 向上转型为Collection接口
Iterable it = coll;             // 向上转型为Iterable接口
  • 接口的default关键字标记的方法
    当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default关键字标记的方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。

default方法和抽象类的普通方法不同:interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。

default方法可以有多个,它仅仅是表示实现接口的子类可以不实现该方法。

  • 接口的字段
    接口还可以有字段,字段的值必须有初始化值。接口的字段属于静态属性字段,因为interface是一个纯抽象类,所以它不能定义实例字段。但是,interface是可以有静态字段的,并且静态字段必须为final类型,实际上,因为interface的字段只能是public static final类型。
public interface Person {
    public static final int MALE = 1;
    public static final int FEMALE = 2;
}

所以我们可以把这些修饰符都去掉,上述代码可以简写为如下,编译器会自动把该字段变为public static final类型。

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

四. Java包

  • 在编写类的时候,在Java文件的开头,会指定当前类的路径,路径的斜线用点来代替,包名的起始从src开始。在Java虚拟机执行的时候,JVM只看完整类名,因此,只要包名不同,类就不同。如下表示类Person的包名为ming.util,路径为src/ming/util/Person.java。要特别注意:包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。
package ming.util;

public class Person {
}

编译class文件,编译的class文件也会根据包的路径,生成对应的不良结构。

$ cd src/
$ mkdir bin
$ javac -d bin/ main/java/com/yuchaoshui/note/Main.java
$ tree bin/
bin/
└── com
    └── yuchaoshui
        └── note
            ├── Main.class
            └── Student.class

4.1 包作用域

包作用域是指一个类允许访问同一个 package 下的没有 public、private 修饰的class,以及没有 public、protected、private 修饰的字段和方法。

4.2 包import方法

  • 方法一:直接写出完整类名。
// Person.java
package ming;

public class Person {
    public void run() {
        mr.jun.Arrays arrays = new mr.jun.Arrays();
    }
}
  • 方法二:用import语句,导入小军的Arrays,然后写简单类名。
// Person.java
package ming;

// 导入完整类名:
import mr.jun.Arrays;

public class Person {
    public void run() {
        Arrays arrays = new Arrays();
    }
}
  • 方法三:写import的时候,可以使用*,表示把这个包下面的所有class都导入进来,但不包括子包的class(不推荐)。
package main;

// 导入System类的所有静态字段和静态方法:
import static java.lang.System.*;

public class Main {
    public static void main(String[] args) {
        // 相当于调用System.out.println(…)
        out.println("Hello, world!");
    }
}
编译器遇到一个class名称时

如果是完整类名,就直接根据完整类名查找这个class;

如果是简单类名,按下面的顺序依次查找:

  • 查找当前package是否存在这个class;
  • 查找import的包是否包含这个class;
  • 查找java.lang包是否包含这个class。
自动import

编写class的时候,编译器会自动帮我们做两个import动作:

  • 默认自动import当前package的其他class;
  • 默认自动import java.lang.*。注意:自动导入的是java.lang包,但类似java.lang.reflect这些包仍需要手动导入。
包名冲突

如果有两个class名称相同,例如,mr.jun.Arrays和java.util.Arrays,那么只能import其中一个,另一个必须写完整类名。

🔗

文章推荐