Skip to content

面向对象编程

面向对象编程有三大特征:封装、继承和多态

类的五大成员:(1) 属性 (2) 方法 (3) 构造器 (4) 代码块 (5) 内部类

访问修饰符

Java一共提供了四种访问控制修饰符,用于控制方法和属性(成员变量)的访问权限(范围):

  • public:公开级别,对外公开
  • protected:受保护级别,对子类和同一个包中的类公开
  • 默认级别:没有修饰符号,向同一个包类公开
  • private:私有级别,只有类本身可以访问,不对外公开

image-20250330204651506

注意事项:

  • 修饰符可以用来修饰类中的属性,成员方法以及类
  • 只有默认的和public修饰符才能修饰类,其他两个修饰符不能修饰类,并且遵循上述访问权限的特点
  • 成员方法的访问规则和属性完全一样

对于类中的四个属性,分别使用不同的访问修饰符进行修饰:

java
package com.jlctest.modifier;

public class A {
    public int  n1 = 100;
    protected int n2 = 200;
    int n3 = 300;
    private int n4 = 400;
    
    // 本类的方法是可以访问到上述不同修饰符的属性
    public void m1() {
        System.out.println(n1, n2, n3, n4);
    }
}

在同包中创建一个B类:

java
package com.jlctest.modifier;

public class B {
    A a = new A();    // 同一个包中,不需要对同包下的类进行引入
    // 在同一个包下,可以访问 public、protected和默认的属性或方法,但是不能访问私有的属性或方法
    System.out.println(a.n1, a.n2, a.n3);   // a.n4访问不到
}

在不同包中创建一个C类:

java
package com.jlctest.modifier2;

public class B {
    A a = new A();    // 同一个包中,不需要对同包下的类进行引入
    // 在不同包下,只能访问 public修饰的属性或方法,不能访问protected、默认的和私有的属性或方法
    System.out.println(a.n1);   // a.n2   a.n3  a.n4访问不到
}

封装

封装就是把抽象出的数据(属性)和对数据操作的方法封装在一起,数据被保护在内部,程序的其他部分只有通过被授权的操作(方法),才能对数据进行操作。对电视机的封装操作就是典型的封装,开关等按键被暴露出来了

封装有以下的好处:

  • 隐藏了实现细节,只需在调用时传递参数即可
  • 可以对数据进行验证(对于一些非常不合理的数据进行验证处理,如age = 1000;,显然是不合理的),保证安全合理

封装步骤

  1. 将属性进行私有化private,在外部不能直接的修改属性

  2. 提供一个公共(public)的set方法,用于属性判断并赋值

    java
    public void setXxx(类型 参数名) {  // xxx表示某个属性
        // 加入数据验证的业务逻辑
        属性 = 参数名;
    }
  3. 提供一个公共(public)的get方法,用于获取属性的值

    java
    public XX getXxx() {   // xxx表示某个属性
        // 权限判断
        return xx;
    }

封装小案例:封装一个Person类,不能随便查看人的年龄,工资等隐私,并对设置的年龄进行合理的验证(1-120岁之间),年龄合理就设置,否则使用默认的年龄,工资不能直接查看,name的长度在2-6个字符之间,具体封装如下:

java
package com.jlctest.encap;

public class Encapsulation01 {
    Person person = new Person();
    person.setName("jlc");
    person.setAge("25");
    person.setSalary(20000);
    // 不能通过person.salary进行访问当前对象的薪水,只能通过person.getSalary()进行薪水的访问
    // 读取信息
    System.out.println(person,info());
}

class Person {
    public String name;   // 名字公开
    private int age;     // 年龄私有化
    private double salary;  // 工资私有化
    
    // 给每个属性提供set和get方法,可以使用快捷键alt+insert,选择Getter and Setter
    // 之后根据要求来完善代码
    public void setName(String name) {
        // 对名字长度的校验
        if (name.length() >= 2 && name.length() <= 6) {
            this.name = name;
        }
        else {
            System.out.println("名字长度不合法");
            // 设置默认的名字
            this.name = "null";
        }
    }
    public String getName() {
        return name;
    }
    public void setAge(int age) {
        // 合理范围判断
        if (age >= 1 && age <= 120) {
            this.age = age;
        }
        else {
            System.out.println("年龄要在1-120之间");
            // 不符合范围设置给定的默认值
            this.age = 18;
        }
    }
    public int getAge() {
        return age;
    }
    public void setSalary(double salary) {
        this.salary = salary;
    }
    public double getSalary() {
        // 可以在这里增加对当前对象的权限判断
        return salary;
    }
    
    // 写一个方法,返回属性信息
    public String info() {
        return "信息为: name:" + name + "age:" + age + "salary:" + salary;
    } 
}

封装结合构造器

使用构造器会导致封装的过滤校验机制失效,如果我们不希望校验方法失效,我们可以将set方法在构造器中进行调用,如:

java
package com.jlctest.encap;

public class Encapsulation01 {
    Person person = new Person();
    person.setName("jlc");
    person.setAge("25");
    person.setSalary(20000);
    // 不能通过person.salary进行访问当前对象的薪水,只能通过person.getSalary()进行薪水的访问
    // 读取信息
    System.out.println(person,info());
    
    // 通过构造器初始化
    Person person1 = new Person("JLC", 125, 20000);
    System.out.println(person1,info());   // JLC 18 20000
}

class Person {
    public String name;   // 名字公开
    private int age;     // 年龄私有化
    private double salary;  // 工资私有化
    
    // 使用构造器
    public Person(String name, int age, double salery) {
        //// 下面方式会使验证失效
        //this.name = name;
        //this.age = age;
        //this.salary = salary;
        // 如果需要用到验证,需要调用set方法
        this.setName(name);
        this.setAge(age);
        this.setSalary(salary);
    }
    
    // 给每个属性提供set和get方法,可以使用快捷键alt+insert,选择Getter and Setter
    // 之后根据要求来完善代码
    public void setName(String name) {
        // 对名字长度的校验
        if (name.length() >= 2 && name.length() <= 6) {
            this.name = name;
        }
        else {
            System.out.println("名字长度不合法");
            // 设置默认的名字
            this.name = "null";
        }
    }
    public String getName() {
        return name;
    }
    public void setAge(int age) {
        // 合理范围判断
        if (age >= 1 && age <= 120) {
            this.age = age;
        }
        else {
            System.out.println("年龄要在1-120之间");
            // 不符合范围设置给定的默认值
            this.age = 18;
        }
    }
    public int getAge() {
        return age;
    }
    public void setSalary(double salary) {
        this.salary = salary;
    }
    public double getSalary() {
        // 可以在这里增加对当前对象的权限判断
        return salary;
    }
    
    // 写一个方法,返回属性信息
    public String info() {
        return "信息为: name:" + name + "age:" + age + "salary:" + salary;
    } 
}

继承

对于两个类的属性和方法,有很多是相同的,这个时候,我们就需要用到继承的概念,实现代码的复用性,同时代码的扩展性和维护性提高了

继承可以解决代码的复用,让我们的编程更加靠近人类的思维,当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过extends来声明继承父类即可,继承的示意图如下:

image-20250331150518850

B类和C类中有很多属性和方法是相同的,我们将B类和C类中共有的属性写在A类中,供B类和C类进行继承,在B类和C类中只写该类特有的属性和方法,我们可以在写一个类D来继承B类,这样D类就同时拥有了B类和A类的所有方法

继承的基本语法:

java
class 子类 extends 父类 {
    ...
}
  • 子类会自动拥有父类定义的属性和方法
  • 父类又叫超类或基类
  • 子类又叫派生类

代码描述:创建一个学生的父类,和小学生子类、大学生子类,父类文件代码:

java
package com.jlctest.extend;

// 父类
public class Student {
    // 共有的属性
    public String name;
    public int age;
    private double score;
    
    // 共有的方法
    public void setScore(double score) {
        this.score = score;
    }
    public void showInfo() {
        System.out.println("学生名:" + name + "年龄:" + age + "成绩:" + score);
    }
}

创建子类进行继承父类和编写子类独特的属性和方法:

java
package com.jlctest.extend;

// 小学生子类 Pupil继承Student父类
public class Pupil extends Student {
    public void testing() {
        System.out.println("小学生" + name + "正在考试...");
    }
}
java
package com.jlctest.extend;

// 大学生子类 Graduate继承Student父类
public class Graduate extends Student {
    public void testing() {
        System.out.println("大学生" + name + "正在考试...");
    }
}

测试文件:

java
package com.jlctest.extend;

public class Extends01 {
    public static void main(String[] args) {
        com.jlctest.extend.Pupil pupil = new Pupil();
        pupil.name = "abc";
        pupil.age = 12;
        pupil.testing();
        pupil.setScore(60);
        pupil.showInfo();
    }
}

注意事项:

  • 子类继承了所有的属性和方法,但是私有属性不能在子类中直接访问(但是可以间接访问的),要通过父类提供的公共方法去访问

    代码解释:先创建一个父类Base

    java
    package com.jlctest.extend;
    
    public class Base {
        public int n1 = 100;    // 公共属性
        protected int n2 = 200; // 受保护属性
        int n3 = 300;           // 默认属性
        private int n4 = 400;   // 私有属性
        
        // 无参构造器
        public Base() {
            System.out.println("Base...");
        }
        // 公共方法
        public void test100() {
            System.out.println("test100...");
        }
        // 受保护方法
        protected void test200() {
            System.out.println("test200...");
        }
        // 默认方法
        void test300() {
            System.out.println("test300...");
        }
        // 私有方法
        private void test400() {
            System.out.println("test400...");
        }
        
        // 父类提供一个公共的方法去返回私有的属性
        public int getN4() {
            return n4;
        }
        
        // 父类提供一个公共的方法去返回私有的方法
        public void callTest400() {
            test400();
        }
    }

    创建一个子类Sub继承父类Base

    java
    package com.jlctest.extend;
    
    public class Sub extends Base {
        // 子类的构造器
        public Sub() {
            System.out.println("Sub...");
        }
        
        public void sayOk() {
            // 非私有的属性和方法可以在子类中直接访问
            System.out.println(n1, n2, n3);   // n4是私有属性,不能直接访问
            // 通过父类提供的公共方法去访问私有的属性
            System.out.println(getN4());
            
            test100();
            test200();
            test300();    // test400()是私有方法,不能直接访问
            // 通过父类提供的公共方法去访问私有的方法
            callTest400();
        }
    }
  • 子类必须调用父类的构造器,完成父类的初始化

    在创建子类的时候,父类的无参构造器也会被调用,会先于子类的无参构造器调用(先完成对父类的初始化)

    在子类中有一个默认的被省略的语句super();,完整的子类构造器语句为:

    java
    package com.jlctest.extend;
    
    public class Sub extends Base {
        // 子类的构造器
        public Sub() {
            super();   // 默认调用父类的无参构造器
            System.out.println("Sub...");
        }
    }
  • 当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类的构造器中用super去指定父类的哪个构造器完成对父类的初始化工作,否则编译不会通过

    java
    package com.jlctest.extend;
    
    public class Base {
        public int n1 = 100;    // 公共属性
        protected int n2 = 200; // 受保护属性
        int n3 = 300;           // 默认属性
        private int n4 = 400;   // 私有属性
        
        // 有参构造器,覆盖了默认的无参构造器
        public Base(int n1) {
            System.out.println("Base(int n1)...");
        }
    }

    子类在调用的时候,需要指定父类中具体的构造器:

    java
    package com.jlctest.extend;
    
    public class Sub extends Base {
        // 子类的构造器
        public Sub() {
            super(150);    // 指定一个构造器合法的实例,显示的调用一下
            System.out.println("Sub...");
        }
    }
  • 如果希望指定去调用父类的某个构造器,则显式的用super()调用一下,如果不写,则默认调用父类的无参构造器

  • super在使用时,必须放在构造器第一行

  • super()this()都只能放在构造器第一行,因此这两个方法不能共存在一个构造器中

  • Java所有的类都是Object类的子类,Object是所有类的基类

  • 父类构造器的调用不限于直接调用上级的父类,可以一直往上追溯到Object类(顶级父类),但是注意Object的无参构造器是没有任何输出的

  • 子类最多只能继承一个父类(指直接继承),即Java中是单继承机制的

    如果想要让A类继承B类和C类,我们可以让A类先继承B类,再让B类去继承C

  • 不能滥用继承,子类和父类之间必须满足is-a(是一个...)的逻辑关系

    Person is a Music? 人不是音乐,不合理,不应该是继承关系

练习题:

java
public class ExtendsExercise01 {
    public static void main(String[] args) {
        B b = new B();   // 执行内容:a    b name   b
    }
}

class A {
    A() {
        System.out.println("a");
    }
    A(String name) {
        System.out.println("a name");
    }
}
class B extends A {
    B() {
        this("abc");    // 执行B(String name){}
        System.out.println("b");
    }
    B(String name) {
        // 默认为super();   // 执行A()
        System.out.println("b name");
    }
}

继承的本质

继承的本质可以帮助我们理解,当子类继承父类时,创建子类对象时,内存中发生了什么(结论:当子类对象创建好后,内存中会建立查找关系)

java
public class ExtendsTheory {
    public static void main(String[] args) {
        Son son = new Son();    // new一个Son时,内存到底发生了什么,内存的布局是怎么样的
        // 当我们使用实例出的子类去访问属性时,是根据什么规则呢?
        System.out.println(son.name);  // 大头儿子
        System.out.println(son.age);  // 45   子类没有,访问的父类的属性
        System.out.println(son.hobby);  // 旅游   子类父类都没有,继续往上找
    }
}

class GrandPa {
    String name = "大头爷爷";
    String hobby = "旅游";
}
class Father extends GrandPa {
    String name = "大头爸爸";
    int age = 45;
}
class Son extends Father {
    String name = "大头儿子";
}

image-20250401133057334

new一个Son类的时候,首先会加载类信息,在加载Son类信息的时候,会先加载顶层的父类信息,也就是Object类,之后依次加载GrandPa的类信息和Father的类信息,等父类的信息加载完后,最后才会加载Son的类信息

加载完类信息之后,会在堆中分配地址空间,首先会给爷爷类分配属性;再会开辟一个空间,为爸爸类分配属性,最后为子类自己开辟空间,分配属性

当我们使用实例出的子类去访问属性时,是根据什么规则呢?我们需要按照查找关系来返回数据,查找规则如下:

  1. 首先看子类是否有目前要访问的属性,如果子类有这个属性,并且可以访问,则返回信息
  2. 如果子类没有这个属性,就看父类有没有这个属性,如果父类有这个属性,并且可以访问,就返回信息
  3. 如果父类没有这个属性,就继续往上找,直到Object

如果父级中的一个属性是私有的,那么子类是不可以进行直接访问的(但是这个私有的属性在内存地址中还是存在的),只是我们需要通过公共的方法进行访问

另外,如果爸爸类中有一个age是私有的,爷爷类中也有一个age是公共的,那我们子类访问age属性会卡在爸爸类中(直接访问报错,即有一个堵住了,不会跳过这个继续往上查找),不会继续往爷爷类去查找(即使爷爷类中的age是私有的)

super关键字

super代表父类的引用,用于访问父类的属性、方法和构造器

  • super可以访问父类的属性(访问方式:super.属性名;),但不能访问父类的私有属性
  • super可以访问父类的方法(访问方式:super.方法名(参数列表);),但不能访问父类的私有方法
  • super可以访问父类的构造器(访问方式:super(参数列表);),只能放在构造器的第一句
java
package com.jlctest.super;

// 父类
public class A {
    // 四个属性
    public int n1 = 100;
    protected int n2 = 200;
    int n3 = 300;
    private n4 = 400;
    
    // 四个方法
    public void test100() {}
    protected void test200() {}
    void test300() {}
    private void test400() {}
}
java
package com.jlctest.super;

// 子类
public class B extends A {
    public void h1() {
        System.out.println(super.n1, super.n2, super.n3);   // super.n4不能访问,报错
    }
    public void ok() {
        super.test100();
        super.test200();
        super.test300();
        // super.test400();不能访问,报错
    }
}

super关键字可以给编程带来便利:

  • 调用父类的构造器的好处:分工明确,父类属性由父类初始化,子类的属性由子类初始化
  • 当子类中有和父类中的成员(属性和方法)重名时,为了访问父类的成员,必须通过supersuper访问属性和方法时,是没有查找本类该属性和方法的过程,直接去父类中进行查找,这个和this和直接访问是有区别的);如果没有重名,使用superthis和直接访问是一样效果的
  • 对于父类中私有的属性和方法,访问会报错:cannot access;如果父类中都没有这个属性和方法,会提示属性或方法不存在
  • super的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用super去访问爷爷类的成员,如果多个基类(上级类)中都有同名的成员,使用super访问遵循就近原则。当然也需要遵循访问权限的相关规则(如私有属性和方法是不能访问的)
superthis的比较

image-20250401151555174

方法重写

方法重写也叫方法覆盖,就是子类有一个方法和父类(这里的父类可以一直追溯到顶级父类)的某个方法的名称、返回类型、形参列表都一样,那么我们就说子类的这个方法覆盖了父类的方法

对于一个Animal父类中,有一个方法:

java
package com.jlctest.override;

public class Animal {
    public void cry() {
        System.out.println("动物叫..");
    }
}

对于子类Dog中,也有cry这个方法:

java
package com.jlctest.override;

public class Dog extends Animal {
    public void cry() {
        System.out.println("狗叫..");
    }
}

因为DogAnimal的子类, Dogcry方法和Animalcry方法的定义形式一样(名称、返回类型和形参列表),这时我们就说Dogcry方法重写了Animalcry方法

在一个测试文件中进行测试:

java
package com.jlctest.override;

public class Test {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.cry();   // 狗叫..
    }
}

方法重写的注意事项:

  • 子类的方法的形参列表,方法名称要和父类方法的形参列表,方法名称和返回类型完全一样,会出现方法的重写

  • 在方法名称和形参列表一样的基础上,如果父类方法的返回类型是子类方法返回类型的父类,也会构成方法的重写(如:父类方法返回的类型是Object,子类方法返回的类型是StringString类型的本质就是Object,如果子类方法返回的类型是Object,父类方法返回的类型是String,系统会直接报错

  • 对于方法的重写,子类方法不能缩小父类方法的访问权限(可以扩大,但是不能缩小)

    对于访问修饰符的权限,由大到小依次为:public>protected>默认>private

    如果父类的方法是public,那么子类的方法必须为public,否则报错

    如果父类的方法是protected,那么子类的方法可以为protectedpublic

  • 对于重写后的方法,如果我们想要重新使用子类的方法,我们可以使用super.父类方法()的形式调用

方法重写和方法重载的比较:

image-20250402151355754


多态

多态,通俗来讲就说多种状态,是面向对象的第三大特征,多态是建立在封装和继承的基础上的

多态可以提高我们代码的复用性,从而利于维护

方法的多态

重写和重载就体现了方法的多态

java
package com.jlctest.Ploy;

public class Ploy {
    public static void main(String[] args) {
        B b = new B();
        A a = new A();
        // 方法的重载体现多态:这里我们传入不同的参数,就会调用不同的sum方法,就体现多态
        System.out.println(a.sum(10, 20));   // 30
        System.out.println(a.sum(10, 20, 30));  // 60
        // 方法重写体现多态:
        a.say();   // A say
        b.say();   // B say
    }
}

// 父类
class B {
    public void say() {
        System.out.println("B say");
    }
}

// 子类
class A extends B {
    public int sum(int n1, int n2) {
        return n1 + n2;
    }
    public int sum(int n1, int n2, int n3) {
        return n1 + n2 + n3;
    }
    
    public void say() {
        System.out.println("A say");
    }
}

对象的多态

对象多态的前提是两个对象(类)存在继承关系

对象的多态是多态的核心:

  • 一个对象的编译类型和运行类型可以不一致(可以使用父类的引用指向子类的对象)
  • 编译类型在定义对象时,就确定了,不能改变
  • 运行类型是可以变化的
  • 编译类型看定义时等号的左边,运行类型看等号的右边
java
// Dog类是Animal类的子类
// 可以使用父类的引用指向子类的对象
Animal animal = new Dog();

animal = new Cat();   // animal的运行类型变成了Cat,但是编译类型仍然是Animal

其中animal的编译类型是Animal,运行类型是Dog

java
// animal的编译类型是Animal,运行类型是Dog
Animal animal = new Dog();
animal,cry(); // 因为运行时,执行到该行时,animal的运行类型是Dog,所以这里的cry方法,就是Dog类中的方法

// 将animal的运行类型改为Cat,但是编译类型还是为Animal
animal = new Cat();
animal.cry();   // 运行类型为Cat,执行的是Cat类中的cry方法

运行类型可以理解为真实的当前类型

多态的向上转型
  • 本质:父类的引用指向了子类的对象

  • 语法:父类类型 引用名 = new 子类类型();

  • 特点:编译类型看左边,运行类型看右边

    可以调用父类中的所有成员(需遵循访问权限)

    不能调用子类中特有成员

    最终运行的效果看子类的具体实现

    java
    Animal animal = new Cat();     // 父类的引用指向了子类的对象
    // 实例出的animal的类型完全是由编译器决定的,遵循的是引用类型Animal
    animal.*;     // 可以调用父类中的所有成员方法(属性和方法)(需遵循访问权限,私有的成员不能访问)
    // 但是不能调用子类中特有的成员(即子类有父类没有的成员)
    animal.catchMouse();  // 报错catchMouse()是Cat类特有的方法
    
    // 最终运行的效果看子类的具体实现,调用方法时,按照从子类开始进行查找,子类没有才往父类进行查找
    animal.eat();    // 根据运行类型,调用Cat类中的eat()方法(如果子类中有这个方法的情况下)    
    animal.run();  // 如果子类中没有run()这个方法,才会去父类中进行找

    在编译阶段,能调用哪些成员(属性和方法),是由编译类型决定的

    但是在最终运行的时候,还是要看子类实现具体的效果

多态的向下转型
  • 语法:子类类型 引用名 = (子类类型) 父类引用;(将一个父类的引用强制转换为一个子类的引用,使其可以调用子类中的特有方法)
  • 只能强转父类的引用,不能强转父类的对象
  • 要求父类的引用必须指向的是当前目标类型的对象
  • 当向下转型后,就可以调用子类类型中的所有成员
java
Animal animal = new Cat();     // 父类的引用指向了子类的对象
animal.catchMouse();  // 报错catchMouse()是Cat类特有的方法(我们使用向下转型来解决)
// 向下转型,强转,将父类的引用重写转换为子类的引用
Cat cat = (Cat) animal;    // 这个时候cat的编译类型和运行类型都是Cat
// 要求父类的引用必须指向的是当前目标类型的对象,也就是说animal原先的指向类型就是Cat类
// 转换完后,就可以调用子类的特有方法
cat.catchMouse();
java
Animal animal = new Cat();     // 父类的引用指向了子类的对象
// 要求父类的引用必须指向的是当前目标类型的对象,也就是说animal原先的指向类型就是Cat类
Cat cat = (Cat) animal;  

Dog dag = (Dog) animal;  // 报错,没有满足父类的引用必须指向的是当前目标类型的对象
属性没有重写

属性没有重写,属性的值看其编译类型

java
package com.jlctest.poly;

public class PloyDetail {
    public static void main(String[] args) {
        Base base = new Sub();   // 向上转型
        System.out.println(base.count);  // 10   看编译类型,编译类型为Base
        
        Sub sub = new Sub();
        System.out.println(base.count);  // 20   看编译类型,编译类型为Sub
    }
}

// 父类
class Base {
    int count = 10;   // 父类的count属性设置为10
}
// 子类
class Sub extends Base {
    int count = 20;   // 子类的count属性设置为20
}
instanceOf比较操作符

instanceOf比较操作符,用于判断对象的运行类型是否为XX类型或者XX类型的子类型

java
package com.jlctest.poly;

public class PloyDetail {
    public static void main(String[] args) {
        // 运行类型和编译类型都为BB
        BB bb = new BB();
        System.out.println(bb instanceOf BB);   // true
        System.out.println(bb instanceOf AA);   // true
        
        // 运行类型为BB   编译类型为AA
        AA aa = new BB();
        System.out.println(aa instanceOf AA);   // true
        System.out.println(aa instanceOf BB);   // true
        
        Object obj = new Object();
        System.out.println(obj instanceOf AA);   // false
    }
}

// 父类
class AA {}
// 子类
class BB extends AA {}

动态绑定机制

Java的动态绑定机制是非常重要的

  1. 当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定
  2. 当调用对象属性时,没有动态绑定机制,哪里声明,哪里调用(在当前的类中查找)
java
package com.jlctest.dynamicBinding;

public class DynamicBinding {
    public static void main(String[] args) {
        A a = new B();  // 向上转型
        // 调用的对象方法看运行类型
        System.out.println(a.sum());  // 40
        System.out.println(a.sum1());  // 30
        
        // 如果将子类B中的sum方法注释掉
        // 当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定,getI()还是从运行类型开始查找
        System.out.println(a.sum());  // 20 + 10 = 30
        // 如果将子类B中的sum1方法注释掉
        // 当调用对象属性时,没有动态绑定机制,哪里声明,哪里调用(在当前的类中查找)
        System.out.println(a.sum1());  // 10 + 10 = 20
    }
}

// 父类
class A {
    pubilc int i = 10;
    public int sum() {
        return getI() + 10;
    }
    public int sum1() {
        return i + 10;
    }
    public int getI() {
        return i;
    }
}
// 子类
class B extends A {
    public int i = 20;
    public int sum() {
        return i + 20;
    }
    public int getI() {
        return i;
    }
    public int sum1() {
        return i + 10;
    }
}

多态的应用

多态数组

多态数组:数组的定义类型为父类类型,里面保存的实际元素类型为子类类型

创建一个Person对象,两个Student对象和两个Teacher对象,统一放在多态数组中,并调用每个对象的say方法

image-20250404194901877

创建Person父类:

java
package com.jlctest.ploy;

public class Person {
    private String name;
    private int age;
    
    // 定义构造器
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 属性的设置和获取方法
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public int getAge() {
        return age;
    }
    
    // 自定义say方法,返回名字和年龄
    public String say() {
        return name + "\t" + age;
    }
}

创建Student子类:

java
package com.jlctest.ploy;

public class Student extends Person {
    private double score;
    
    // 定义构造器
    public Person(String name, int age, double score) {
        super(name, age);
        this.score = score;
    }
    // 属性的设置和获取方法
    public void setScore(double score) {
        this.score = score;
    }
    public String getScore() {
        return score;
    }
    
    // 重写父类的say方法,返回名字和年龄还有分数
    public String say() {
        return super.say() + "score" + score;
    }
    
    // 学生类中特有的方法
    public void study() {
        System.out.println("学生" + getName() + "正在听课");
    }
}

创建Teacher子类:

java
package com.jlctest.ploy;

public class Teacher extends Person {
    private double salary;
    
    // 定义构造器
    public Person(String name, int age, double salary) {
        super(name, age);
        this.salary = salary;
    }
    // 属性的设置和获取方法
    public void setSalary(double salary) {
        this.salary = salary;
    }
    public String getSalary() {
        return salary;
    }
    
    // 重写父类的say方法,返回名字和年龄还有分数
    public String say() {
        return super.say() + "salary" + salary;
    }
    
    // 老师类中特有的方法
    public void teach() {
        System.out.println("老师" + getName() + "正在授课");
    }
}

使用多态数组进行声明和方法的调用:

java
package com.jlctest.ploy;

public static void main(String[] args) {
    // 创建一个Person对象,两个Student对象和两个Teacher对象,统一放在多态数组中
    // Person[]类型的多态数组,其元素子要是Person类和其子类都可以进行存放
    Person[] persons = new Person[5];
    persons[0] = new Person("jack", 20);
    persons[1] = new Student("mary", 18, 80);
    persons[2] = new Student("frank", 19, 100);
    persons[3] = new Teacher("scoot", 35, 18000);
    persons[4] = new Teacher("king", 55, 22000);
    
    // 循环遍历多态数组,调用say方法
    for (int i = 0; i < persons.length; i++) {
        // 使用了动态绑定机制,编译类型是Person,运行类型会根据实际变化
        System.out.println(person[i].say());  
        
        // 这里的persons[] 元素的编译类型是Person,是没有办法调用老师类或者学生类中的特有方法的
        // persons[i].teach();   // 报错
        // 我们要使用向下转型,将编译类型转成具体的子类,再去调用具体的方法
        if (persons[i] instanceof Student) {   // 先判断运行类型是不是Student类型
            Student student = (Student)persons[i];
            student.study();
            // 上述两条语句可以合二为一:  (Student)persons[i].study();
        } else if(persons[i] instanceof Teacher) {
            Teacher teacher = (Teacher)persons[i];
            teacher.teach();
        } else if(persons[i] instanceof Person) {
            // 不做任何处理
        } else {
            System.out.println("类型有误");
        }
    }
}
多态参数

多态参数是指方法定义的形参类型为父类类型,实参类型允许为子类类型

定义员工类Employee,包含姓名和月工资[private],以及计算年工资getAnnual的方法。普通员工和经理继承了员工,经理类多了奖金bonus属性和管理manage方法,普通员工类多了work方法,普通员工类和经理类要求分别重写getAnnual方法:

创建Employee父类:

java
package com.jlctest.ploy;

public class Employee {
    private String name;
    private double salary;
    
    // 定义构造器
    public Person(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }
    // 属性的设置和获取方法
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setSalary(double salary) {
        this.salary = salary;
    }
    public double getSalary() {
        return salary;
    }
    
    // 自定义年工资方法
    public double getAnnual() {
        return 12 * salary;
    }
}

创建一个普通员工Worker子类:

java
package com.jlctest.ploy;

public class Worker extends Employee {
    // 定义构造器
    public Person(String name, double salary) {
        super(name, salary);
    }
    // 普通员工类中特有的work方法
    public void work() {
        System.out.println("普通员工" + getName() + "正在工作");
    }
    
    // 重写getAnnual方法
    public double getAnnual() {
        // 因为普通员工没有其他收入,直接调用父类的方法即可
        return super.getAnnual();
    }
}

创建一个经理Manager子类:

java
package com.jlctest.ploy;

public class Manager extends Employee {
    private double bonus;
    
    // 定义构造器
    public Person(String name, double salary, double bonus) {
        super(name, salary);
        this.bonus = bonus;
    }
    
    public String getBonus() {
        return bonus;
    }
    public void setBonus(double bonus) {
        this.bonus = bonus;
    }
    
    // 经理类中特有的manage方法
    public void manage() {
        System.out.println("经理" + getName() + "正在管理");
    }
    
    // 重写getAnnual方法
    public double getAnnual() {
        return super.getAnnual() + bonus;
    }
}

在测试类中添加一个方法showEmpAnnual(Employee e),实现获取任何员工对象的年工资,并在main方法中调用该方法[e.getAnnual]

在测试类中添加一个方法testWork,如果是普通员工,则调用work方法,如果是经理,则调用manage方法

java
package com.jlctest.ploy;

public class PloyParameter {
    public static void main(String[] args) {
        Worker tom = new Worker("tom", 2000);
        Manager milan = new Manager("milan", 5000, 10000);
        PloyParameter ployParameter = new PloyParameter();
        ployParameter.showEmpAnnual(tom);   // 30000
        ployParameter.showEmpAnnual(milan);   // 70000
        
        ployParameter.testWork(tom);  // 普通员工tom正在工作
        ployParameter.testWork(milan);  // 经理milan正在管理
    }
    
    // 获取任何员工对象的年工资的方法
    public void showEmpAnnual(Employee e) {
        System.out.println(e.getAnnual());  // 使用了动态绑定机制
    }
    
    // 添加一个方法testWork,如果是普通员工,则调用work方法,如果是经理,则调用manage方法
    public void testWork(Employee e) {
        if (e instanceof Worker) {
            ((Worker) e).work();    // 向下转型
        } else if (e instanceof Manager) {
            ((Manager) e).manage();
        }
    }
}

Object根类

Object是类层次结构的根类,每个类都使用Object类作为超类,所有对象(包括数组)都可以使用Object类的方法,Object类中常用的方法有:

方法描述
clone()创建并返回此对象的一个副本
equals()指示其他某个对象是否与此对象"相等"
finalize()当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法
getClass()返回此Object的运行时类
hashCode()返回该对象的哈希码值
toString()返回该对象的字符串表示

equals方法

常见面试题:

==(比较运算符)和equals的对比:

  • ==既可以判断基本类型,又可以判断引用类型

    • ==如果判断基本类型,判断的是值是否相等(是判断值相等,而不深入到类型)

      java
      int num1 = 10;
      double num2 = 10.0;
      System.out.println(num1 == num2);   // ture
    • ==如果判断引用类型,判断的是地址是否相等,即判断是不是同一个对象

      java
      A a = new A();
      A b = a;
      A c = b;
      System.out.println(a == c); // true
  • equals方法是Object类中的方法,只能判断引用类型

    默认判断的是地址是否相等,子类中往往重写该方法,用于判断内容是否相等,比如IntegerString中都会将equals方法进行重写(加入了比较具体的值是否相等)

    java
    // Object中的equals方法,默认就是比较对象地址是否相同,即也就是判断两个对象是不是同一个对象
    public boolean equals(Object obj) {
        // 如果是同一个对象,就返回true
        return (this == obj);
    }
    
    // Integer中的equals方法
    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }
    java
    Integer integer1 = new Integer(1000);
    Integer integer2 = new Integer(1000);
    System.out.println(integer1 == integer2);  // false   开辟了不同的地址空间
    System.out.println(integer1.equals(integer2));  // true   判断具体的值是否相等

    如何查看jdk原码:

    • 一般来说IDEA配置好JDK以后,jdk的源码也就自动配置好了,如果没有,可以点击菜单File-->Project Structure-->SDKs-->Sourcepath,然后点击右侧绿色的加号,选中我们下载的jdk子目录下的javafx-src.zip文件和src.zip文件
    • 在查看某个方法的源码时,将光标放在该方法上,输入ctrl+b或者在该方法上点击右键-->go to-->Declaration or Usages即可跳转到该方法的源码
如何重写equals方法

案例:判断两个Person对象的内容是否相等,如果两个Person对象的各个属性值都一样,则返回true,反之,返回false

java
package com.jlctest.object;

public class EqualsExercise {
    public static void main(String[] args) {
        Person person1 = new Person("jack", 10, '男');
        Person person2 = new Person("jack", 10, '男');
        
        System.out.println(person1.equals(person2));  // false   继承的是Object中的equals
        // 有了自定义的equals方法
        System.out.println(person1.equals(person2));  // ture
    }
}

class Person {
    private String name;
    private int age;
    private char gender;
    
    // 创建构造器
    public Person(String name, int age, char gender) {
        this.name = name;
        this,age = age;
        this.gender = gender;
    }
    
    // 重写Object的equals方法
    public boolean equals(Object obj) {
        // 判断如果比较的两个对象是同一个对象,则直接返回true
        if (this == obj) {
            return true;
        }
        // 类型判断,是Person,再进行比较
        if (obj instanceof Person) {
            // 进行向下转型,获取具体的属性
            Person p = (Person)obj;
            // 判断全部属性是否相等
            return this.name.equals(p.name) && this.age==p.age && this.gender==p.gender;
        }
        // 如果不是Person对象,则直接返回false
        return false;
    }
}

hashCode方法

hashCode方法用于返回该对象的哈希码值(哈希码值是为了提高哈希表的性能)

该方法主要有以下几个方面:

  • 提高具有哈希结构的容器的效率
  • 两个引用,如果指向的是同一个对象,则哈希值是一样的
  • 两个引用,如果指向的是不同的对象,则哈希值是不一样的
  • 哈希值主要根据地址号来计算的,但是不能完全将哈希值等价于地址
  • 在后续的集合中,一般会将hashCode方法进行重写
java
package com.jlctest.object;

public class HashCode {
    public static void main(String[] args) {
        AA aa1 = new AA();
        AA aa2 = new AA();
        AA aa3 = aa1;
        // 此时  aa1.hashCode和aa2.hashCode的值是不一样的,但是与aa3.hashCode的值是一样的
    }
}

class AA {}

toString方法

返回该对象的字符串表示,具体而言,就是返回一个以文本方式表示的此对象字符串

默认返回:全类名(包名+类名)+@+哈希值的十六进制

方法的源码:

java
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

在子类中往往重写toString方法,用于返回对象的属性信息

当直接输出一个对象时,toString方法会被默认的调用,如System.out.print(monster);等价于System.out.print(monster.toString());

finalize方法

  • 当对象被回收时(在堆中的空间被释放出来了),系统自动调用该对象的finalize方法,子类可以重写该方法,做一些释放资源的操作
  • 什么时候被回收:当某个对象没有任何引用时,则jvm就认为这个对象使一个垃圾对象,就会使用垃圾回收机制来销毁该对象,在销毁对象之前,会先调用finalize方法
  • 垃圾回收机制的调用,是由系统来决定的(即有自己的GC算法),也可以通过System.gc()主动触发垃圾回收机制
java
package com.jlctest.object;

public class Finalize {
    public static void main(String[] args) {
        Car bmw = new Car("宝马");
        // 将对象的指向连线断掉,即对象没有任何引用了,这个Car对象就变成了垃圾,被回收
        // 在回收前(销毁前),会调用该对象的finalize方法
        // 这时程序员就可以在finalize方法中写一些自己业务逻辑的方法(如释放资源:数据库连接或者打开文件)
        // 如果程序员不重写finalize,那么就会调用Object类的finalize,即默认调用
        bwm = null;
        System.gc();  // 主动触发垃圾回收
        System.out.println("程序退出了")
    }
}

class Car {
    private String name;
    publuc Car(String name) {
        this.name = name;
    }
    // 重写finalize方法
    @Override
    protected void finalize() throws Throwable {
        System.out.println("我们销毁了汽车" + name);
    }
}

最后结果显示:程序退出了 我们销毁了汽车宝马

finalize方法在实际的开发中使用的比较少,但是在面试中问的比较多


类变量和类方法

类变量和类方法是面向对象的一个重点,类变量和类方法,又叫静态变量和静态方法,用static关键字进行修饰

类变量

类变量也叫静态变量/静态属性,是该类的所有对象共享的变量,任何一个该类的对象去访问它时,取到的都是相同的值,同样任何一个该类的对象去修改它时,修改的也是同一个变量

定义类变量:

  • 访问修饰符 static 数据类型 变量名; 推荐使用
  • static 访问修饰符 数据类型 变量名;
java
class Child {
    private String name;
    // 定义一个类变量为所有的Child类的对象共享
    public static int totalNum = 0;
}

类变量的访问:静态变量的访问修饰符的访问权限和范围和普通属性是一样的

  • 类名.类变量名; 推荐使用
  • 对象名.类变量名;

类变量可以通过对象进行访问,也可以通过类名进行访问Child.totalNum;

java
package com.jlctest.static;

public class VisitStatic {
    public static void main(String[] args) {
        // 类变量是随着类的加载而创建的,所以即使没有创建对象实例也可以访问
        System.out.println(A.name);   // jlc
        
        // 也可以通过一个对象实例进行访问
        A a = new A();
        System.out.println(a.name);   // jlc
    }
}

class A {
    // 定义类变量
    public static String name = "jlc";
}

类变量的使用细节:

  • 当我们需要让某个类的所有对象都共享一个变量时,就可以考虑使用类变量(静态变量)
  • 加上static就称为类变量\静态变量,否则就为实例变量\普通变量\非静态变量
  • 类变量是该类的所有对象共享的,而实例变量(普通属性)是对每个对象独享的
  • 实例变量不能通过类名.变量名的方式访问
  • 类变量是随着类的加载而创建的,所以即使没有创建对象实例也可以访问
  • 类变量的生命周期是随着类的加载开始,随着类的消亡而销毁
内存布局

image-20250405191010103

  • 类变量count是被该类实例化出来的对象共享的(静态变量是被对象共享的)都指向静态变量所在的空间(堆空间)
  • jdk的版本如果大于等于8,静态对象是放在堆中的,通过反射机制会加载一个Class对象(在方法区中类加载之后,会通过反射机制生成一个Class对象实例),在对象实例的最后,会将这个静态数据放入;jdk的版本如果小于8,静态对象放在方法区中的,静态变量在加载的时候,有一个类加载,类信息会放在方法区中,就会在方法区中产生一个静态域来存放这个类变量,但是不管静态变量在哪里,都是可以被对象共享的
  • 类变量,在类加载的时候就生成了

类方法

类方法也叫静态方法

基本语法:

  • 访问修饰符 static 数据返回类型 方法名() {} 推荐使用
  • static 访问修饰符 数据返回类型 方法名() {}

类方法的调用:类名.类方法名或者对象名.类方法名 调用的前提是满足访问修饰符的访问权限和范围

java
package com.jlctest.static;

public class StaticMethod {
    public static void main(Stringp[] args) {
        // 创建两个学生对象,交学费
        Stu tom = new Stu("tom");
        tom.payFee(1000);    // 通过对象来调用静态方法,也可以使用类名进行调用
        
        Stu mary = new Stu("mary");
        mary.payFee(2000);    // 通过对象来调用静态方法,也可以使用类名进行调用
        
        // 输出当前收到的总学费,通过类进行调用静态方法
        Stu.showFee();    // 总学费为:3000.0
    }
}

class Stu {
    private String name;  // 普通私有成员
    // 定义一个静态变量,来累积学生的学费
    private static double fee = 0;
    
    public Stu(String name) {
        this.name = name;
    }
    
    // 创建静态方法    静态方法可以访问静态属性/变量
    public static void payFee(double fee) {
        Stu.fee += fee;   // 学费累积
    }
    public static void showFee() {
        System.out.println("总学费为:" + Stu.fee);
    }
}

类方法的使用场景:当方法中不涉及到任何和对象相关的成员,则可以将方法设计成静态方法,提高开发效率,如工具类中的方法utilsMath类、Arrays类等,如Math.sqrt(),其中的sqrt()就是一个静态方法,我们可以直接通过类进行调用(如果我们希望不创建实例,也可以调用某个方法,即当作工具来使用,这时将方法做成静态方法是非常合适的)

在程序员实际开发中,往往会将一些通用的方法,设计成静态方法,这样我们就可以不需要创建对象就直接使用

类方法使用的注意事项:

  • 类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区:类方法中无this参数,普通方法隐含着this参数

  • 类方法可以通过类名调用,也可以通过对象名调用

  • 普通方法和对象有关,需要通过对象名调用,不能通过类名调用

  • 类方法中不允许使用和对象有关的关键字,比如thissuper,普通方法(成员方法)可以

  • 类方法中只能访问静态成员;普通成员方法既可以访问普通成员,也可以访问静态成员(构造器方法也属于普通方法,可以方法普通成员和静态成员)(但是两种方法访问都必须遵守访问权限)

    java
    class D {
        private int n1 = 100;   // 普通变量
        private static int n2 = 200;  // 静态变量
        
        public static void hello() {
            System.out.println(n2);  // 合法
            System.out.println(D.n2);  // 合法
            System.out.println(this.n2);  // 不合法,类方法中不允许使用和对象有关的关键字
            System.out.println(n1);  // 不合法,不能访问普通变量(方法)
        }
    }

main方法

main方法的语法形式:public static void main(String[] args) {}

  • main()方法是Java虚拟机在调用

  • Java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是public

  • Java虚拟机在执行main()方法时不必创建对象,所以该方法必须是static

  • 该方法接收String类型的数组参数,该数组中保存执行Java命令时传递给所运行的类的参数

  • Java执行的程序 参数1 参数2 参数3

    image-20250408105300053

    java
    public class Hello {
        public static void main(String[] args) {
            // 遍历args接收传入的参数
            for(int i = 0; i < args.length; i++) {
                System.out.println("第" + (i + 1) + "个参数为" + args[i]);
            }
        }
    }

    编译代码:javac Hello.java

    运行代码并传入参数:java Hello jlc abc qwer (如果不传入参数,就什么也不会输出)

    结果显示:

    第1个参数为jlc

    第2个参数为abc

    第3个参数为qwer

    参数在执行程序的时候进行传入,在传入参数后,会将jlc abc qwer这三个参数当作一个字符串数组String[]传给我们的方法

注意事项:

  • main()方法中,我们可以直接调用main方法所在类的静态方法或静态属性,但是不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态方法

代码块

代码块又称为初始化块,属于类中的成员(即是类的一部分),类似于方法,将逻辑语句封装在方法体中,通过{}包围起来;但是和方法不同,代码块没有方法名,没有返回,没有参数,只有方法体(代码块可以理解为只有方法体的方法),而且不用通过对象或类显示调用,而是在加载类时,或创建对象时隐式调用

基本语法:

java
修饰符 {
    代码;
};
  • 修饰符可选,要写的化,也只能写static
  • 代码块分为两类,使用static修饰的静态代码块,和没有static修饰的普通代码块
  • 代码块中的语句可以为任何逻辑语句(输入、输出、方法调用、循环、判断等)
  • 结尾的;可以省略

使用代码块的好处:代码块相当于另一种形式的构造器(对构造器的补充机制),可以做初始化的操作

应用场景:如果多个构造器中都有重复的语句,可以抽取到初始化块中,提高代码的复用性

java
public class CodeBlock {
    public static void main(String[] args) {
        Movie movie1 = new Movie("movie1");  
    }
}

class Movie {
    private String name;
    private double price;
    
    // 2个构造器,构成了重载
    // 两个构造器中存在相同的内容,这样代码看起来比较冗余,我们可以将相同的语句,放到一个代码块中
    {
        System.out.println("电影屏幕打开...");
        System.out.println("电影开始...");
    };
    // 不管我们调用哪个构造器,创建对象,都会调用代码块中的内容
    public Movie(String name) {
        System.out.println("Movie(String name)构造器被调用");
        this.name = name;
    }
    public Movie(String name, double price) {
        System.out.println("Movie(String name, double price)构造器被调用");
        this.name = name;
        this.price = price;
    }
}

结果显示:

电影屏幕打开...

电影开始...

Movie(String name)构造器被调用

代码块的调用的顺序是优先于我们的构造器调用的

代码块使用注意事项:

  • static代码块也叫静态代码块,作用是对类进行初始化,而且它随着类的加载而执行,并且只会执行一次。如果是普通代码块,每创建一个对象,就执行

  • 类什么时候被加载(重要):

    • 创建对象实例时(new
    • 创建子类对象实例,父类也会被加载,如果子类和父类中都有代码块,父类的代码块会先被执行
    • 使用类的静态成员时(静态属性,静态方法),代码块中的内容会先被执行
    • 使用类的静态成员时(静态属性,静态方法),如果这个子类的父类有代码块,也会被加载执行,而且父类的代码块会先被执行
  • 普通代码块,在创建对象实例时,会被隐式的调用,被创建一次,就会被调用一次;如果只是使用类的静态成员时,普通代码块并不会执行

  • 创建一个对象时,在一个类调用的顺序是(由先到后):(重点和难点)

    1. 调用静态代码块和静态属性初始化

      静态代码块和静态属性初始化调用的优先级一样,如果有多个静态代码块和多个静态变量初始化,则按它们定义的顺序调用

      java
      public class CodeBlock {
          public static void main(String[] args) {
              A a = new A();  // 输出顺序:(1)getN1被调用... (2)A 静态代码块
          }
      }
      
      class A {
          // 静态属性初始化
          private static int n1 = getN1();
          // 静态代码块
          static {
              System.out.println("A 静态代码块");
          }
          public static int getN1() {
              System.out.println("getN1被调用...");
              return 100;
          }
      }
    2. 调用普通代码块和普通属性初始化

      普通代码块和普通属性初始化调用的优先级一样,如果有多个普通代码块和多个普通变量初始化,则按它们定义的顺序调用

    3. 调用构造器方法

  • 构造器的代码最前面是隐含了super()和调用普通代码块

    java
    class A {
        public A() {
            System.out.println("ok");
        }
        
        // 上述构造器有隐藏的执行要求
        public A() {
            // (1)调用父类的无参构造器
            // 如果父类中也有其本类的普通代码块,在调用父类构造器时,会先进行父类中普通代码块的调用
            super();
            // (2)调用本类普通代码块的隐藏逻辑
            System.out.println("ok");
        }
    }
  • 在创建一个子类对象时(有继承关系),它们的静态代码块,静态属性初始化,普通代码块,普通属性初始化,构造方法的调用顺序为(由先到后):(高频面试题)

    1. 父类的静态代码块和静态属性(优先级一样,按照定义顺序执行)
    2. 子类的静态代码块和静态属性(优先级一样,按照定义顺序执行)
    3. 父类的普通代码块和普通属性初始化(优先级一样,按照定义顺序执行)
    4. 父类的构造方法
    5. 子类的普通代码块和普通属性初始化(优先级一样,按照定义顺序执行)
    6. 子类的构造方法

    1和2,是类加载时必须会执行的;后面4步是与对象相关的内容(入口是对应子类的构造器,遵守构造顺序)

  • 静态代码块只能直接调用静态成员(静态属性和静态方法),普通代码块可以调用任意成员


final关键字

final关键字可以修饰类、属性、方法和局部变量

有以下的四个情况会使用到final关键字:

  • 当不希望类被继承时,可以用final修饰(希望这个类是最后的类,不希望被其他类继承扩展)

    java
    // 如果我们要求A类不能被其他类继承,使用final关键字修饰即可
    final class A {}
    
    // 这样其他类继承就会直接报错
    class B extends A {}  // 报错
  • 当不希望父类的某个方法被子类覆盖/重写时,可以使用final关键字修饰

    java
    // 如果我们不希望父类的某个方法被子类覆盖/重写时,使用final关键字修饰即可
    // A作为父类
    class A {
        // 不希望hi()方法被子类覆盖/重写,使用final关键字修饰即可
        public final void hi() {}
    }
  • 当不希望类的某个属性值被修改,可以用final关键字修饰

    java
    class A {
        // 不希望某个属性值被修改,使用final关键字修饰即可
        public final double TAX_RATE = 0.08;
    }
  • 当不希望某个局部变量被修改,可以使用final关键字修饰

    java
    class A {
        public void hi() {
            // 不希望某个局部变量被修改,使用final关键字修饰即可
            final double NUM = 0.01;   // 这时NUM也被称为局部常量
            NUM = 0.2; // 报错
        }
    }

注意事项:

  • final修饰的属性又叫常量,一般用XX_XX_XX来命名

  • final修饰的属性在定义时,必须赋初值,并且以后不能再修改,赋值可以在下面几个位置:

    • 定义时:如:public final double TAX_RATE = 0.08;

    • 在构造器中

      java
      class A {
          // 在构造器中赋初值
          public final double TAX_RATE;
          
          public A() {
              TAX_RATE = 0.08;
          }
      }
    • 在代码块中

      java
      class A {
          // 在代码块中赋初值
          public final double TAX_RATE;
          
          {
              TAX_RATE = 0.08;
          }
      }
  • 如果final修饰的属性是静态的,则初始化的位置只能是在定义时和在静态代码块中,不能在构造器中赋值

    java
    class A {
        public static final double TAX_RATE1 = 0.02;
        
        public static final double TAX_RATE2;
        static {
            TAX_RATE2 = 0.08;
        }
    }
  • final类不能继承,但是可以实例化出具体的对象

  • 如果类不是final类,但是含有final方法,则该方法虽然不能重写,但是可以被继承下来供子类进行使用

  • 一般来说,如果一个类已经是final类了,就没有必要再将方法修饰成final方法了(因为这个类已经是不能被继承了,就没有子类会去重写这个方法了)

  • final不能修饰构造方法(即构造器)

  • finalstatic往往搭配使用(两者的前后顺序可以改变),效率更高,底层编译器做了优化处理,不会导致类加载

    java
    class B {
        public static int num = 1000;
        public static int num2 = 2000;
        static {
            System.out.println("B类的静态代码块被执行");
        }
    }
    
    // 在主函数调用B类中的属性时,会加载类,先执行代码块中的内容
    System.out.println(B.num);   // B类的静态代码块被执行   1000
    
    // 当final和static一起使用时,不会加载类,不会执行代码块中的内容
    System.out.println(B.num2);  // 2000
  • 对于类中的方法,其形参是可以用final修饰的,但是这样的形参是不能修改的

    java
    public int add(final int x) {
        // x的值不能修改
    }
  • 对于包装类(IntegerDoubleFloatBoolean等都是通过final修饰的),String也是final类,这些类都是不能被继承的


抽象类

当父类中的某些方法需要声明时,但是又不确定如何具体的实现,可以将其声明为抽象方法,那么这个类为抽象类

java
abstract class Animal {
    private String name;
    
    public Animal(String name) {
        this.name = name;
    }
    // 这里实现了eat方法,但是实际上没有什么意义,后续在子类中都是要重写的(父类方法不确定性的问题)
    // 即将该方法设置为抽象方法
    //public void eat() {
    //    System.out.println("这是一个吃的方法,但是不知道具体吃什么");
    //}
    public abstract void eat();
}

抽象方法就是没有实现的方法,具体而言,就是没有方法体,这个具体的方法会让子类进行实现(一般来说,抽象类会被继承,由其子类来实现抽象方法)

将方法设置为抽象方法后,必须要将该类设置为抽象类,否则会报错

注意事项:

  • abstract关键字来修饰一个类时,这个类就是一个抽象类
  • 抽象类是不能被实例化的
  • 抽象类不一定要包含abstract方法,也就是说,抽象类可以没有abstract方法
  • 一旦类包含了abstract方法,则这个类必须声明为abstract
  • 抽象类可以有任意成员(因为抽象类的本质还是类),比如:非抽象方法、构造器、静态属性等
  • abstract关键字来修饰一个方法时,这个方法就是一个抽象方法,该方法是没有方法体的,即没有{}
  • abstract只能修饰类和方法,不能修饰属性和其他的内容
  • 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法(对父类的抽象方法增加一个方法体{},就表示实现了父类的方法),除非它自己也声明为抽象类
  • 抽象类的价值更多的作用是在于设计,是设计者设计好后,让子类继承并实现抽象类
  • 抽象方法不能使用privatefinalstatic来修饰,因为这些关键字都是和重写相违背的

接口

接口就是给出一些没有实现的方法,封装到一起,到某个类要使用的时候,在根据具体情况把这些方法写出来

基本语法:

java
// 声明接口
interface 接口名 {
    // 可以写属性
    public int n1 = 10;
    
    // 可以写抽象方法,注意:在接口中,写抽象方法时,可以省略abstract关键字
    public void hi();
    
    // jdk8.0后,接口可以使用静态方法
    public static void cry() {
        System.out.println("cry");
    }
    
    // jdk8.0后,接口可以使用默认方法,但是要使用default关键字进行修饰
    default public void ok() {
        System.out.println("ok");
    }
}

// 类去使用接口
class 类名 implements 接口 {
    自己属性;
    自己方法;
    必须实现的接口的抽象方法;   // 如果一个类implements接口,需要将该接口的所有抽象方法都实现
}
  • jdk7.0前,接口里的所有方法都没有方法体,即都是抽象方法
  • jdk8.0后,接口可以有静态方法,默认方法,也就是说接口中可以有方法的具体实现,但是默认方法要使用default关键字进行修饰

注意事项:

  • 接口不能被实例化

  • 接口中所有的方法都是public方法(public修饰符可以忽略),接口中抽象方法,可以不用abstract修饰

  • 一个普通类实现接口,就必须将该接口的所有方法都实现(重写),可以使用alt+enter快捷键来快速实现

  • 抽象类实现接口,可以不用实现接口的方法

  • 一个类同时可以实现多个接口,但是也要完成这些接口中的所有抽象方法

  • 接口中的属性,只能是final的,而且是public static final修饰符,比如:

    int a = 1;实际上为public static final int a = 1;(必须初始化)

    • 因为接口中的属性都是静态的,所以接口中属性的访问形式:接口名.属性名
    • 因为接口中的属性是final,所以后续我们不可以在外界修改接口中的属性值
  • 接口不能继承其它的类,但是可以继承多个别的接口:interface A extends B,C {}

  • 声明接口的修饰符只能是public和默认,这点和类的修饰符一样

java
interface A {
    int a = 23;   // 等价于 public static final int a = 23;
}

class B implements A {}

// 在main方法中
B b = new B();
System.out.println(b.a);  // 23   对象实例去访问公共属性
System.out.println(A.a);  // 23   接口去访问内部的属性
System.out.println(B.a);  // 23   B类实现了A接口,可以使用A接口中的属性

接口的使用场景

我们在具体什么时候需要使用接口,通常有以下的几个场景:

  • 以制造飞机为例:对于各种种类的飞机,专家只需要把飞机需要的功能/规格定下来即可,具体的部分让别人实现即可
  • 对于一个项目经理来说,管理三个程序员,要开发一个软件,为了控制和管理软件,项目经理可以定义一些接口,然后由程序员具体实现,这样可以更好的管理使用和统一命名规范

实现接口和继承类

实现接口和继承类的区别:Java提供的实现接口机制,可以理解为对单继承机制的补充

  • 继承的价值主要在于:解决代码的复用性和可维护性
  • 接口的价值主要在于:设计好各种规范(方法),让其它类去实现这些方法,即更加灵活
  • 接口比继承更加灵活,继承是满足is-a(是什么)的关系,而接口只需满足like-a(像什么)的关系
  • 接口在一定程度上实现代码的解耦
java
package com.jlctest.interface;

public class ExtendsVsInterface {
    public static void main(String[] args) {
        LittleMonkey wukong = new LittleMonkey("climbing");
        wukong.climbing();   // wukong猴子会爬树
        wukong.swimming();   // wukong通过学习,猴子会游泳
        wukong.flying();     // wukong通过学习,猴子会飞翔
    }
}

// 鱼类接口
interface Fishable {
    void swimming();
}

// 小鸟接口
interface Birdable {
    void flying();
}

// 猴子类
class Monkey {
    private String name;
    public Monkey(String name) {
        this.name = name;
    }
    public void climbing() {
        System.out.println(name + "猴子会爬树");
    }
    public String getName() {
        return name;
    }
}

// 小猴子类去继承猴子类
class LittleMonkey extends Monkey implements Fishable, Birdable {
    public LittleMonkey(String name) {
        this.name = name;
    }
    
    @Override
    public void swimming() {
        System.out.println(getName() + "通过学习,猴子会游泳");
    }
    
    @Override
    public void flying() {
        System.out.println(getName() + "通过学习,猴子会飞翔");
    }
}

对于继承,只要是继承了这个父类,其子类就可以自动的使用父类中的能力(公共方法);同时继承是单继承的,只能继承一个父类,不同同时继承多个父类

通过实现接口,可以无形的将子类的功能进行加强(如果子类需要拓展功能,可以通过实现接口的方式扩展),Java提供的实现接口机制,可以理解为对单继承机制的补充

综合小练习:

java
interface A {
    int x = 0;    // 等价于public static final int x = 0;
}

class B {
    int x = 1;   // 普通属性
}

class C extends B implements A {
    public void pX() {
        // System.out.println(x);  // 错误,原因不明确x到底是接口中的还是类中的
        // 访问接口中的x就使用A.x   访问父类的x就使用super.x
        System.out.println(A.x);
        System.out.println(super.x);
    }
}

接口的多态特性

多态参数

接口可以体现多态参数

java
package com.jlctest.interface;

public class Computer {
    // 形参是接口类型 UsbInterface
    public void work(UsbInterface usbInterface) {
        // 通过接口,来调用方法
        usbInterface.start();
    }
}

在测试文件中使用work方法:

java
package com.jlctest.interface;

public class interfaceTest {
    public static void main(String[] args) {
        // 创建手机对象,Phone类实现了UsbInterface接口
        Phone phone = new Phone();
        // 创建计算机对象
        Computer computer = new Computer();
        
        // 将手机接入到计算机中,通过多条参数,UsbInterface接口的对象实例是可以传递给work方法的
        computer.work(phone);    // 只要是实现了UsbInterface接口的对象实例,都可以进行传递
    }
}

接口引用可以指向实现了接口的类的对象

java
package com.jlctest.interface;

public class InterfacePolyParameter {
    public static void main(String[] args) {
        // 接口的多态体现(与继承体现的多态类似)
        // 接口类型的变量 if01 可以指向 实现了IF接口类的对象实例
        IF if01 = new Monster();
        if01 = new Car();
    }
}

// 声明接口
interface IF {}
// 声明两个类去使用接口
class Monster implements IF {}
class Car implements IF {}
多态数组

多态数组指的是接口类型的数组,可以存放实现了这个接口的类的实例

java
package com.jlctest.interface;

public class InterfacePolyArr {
    public static void main(String[] args) {
        // 多态数组 -> 接口类型数组
        // Usb多态数组,存放Phone和Camera对象
        Use[] usbs = new Usb[2];   // 声明多态数组空间
        usbs[0] = new Phone();
        usbs[1] = new Camera();
        
        // 遍历Usb多态数组,如果是Phone对象,除了调用Usb接口定义的方法体外,还要调用特有的call方法
        for(int i = 0; i < usbs.length; i++) {
            usbs[i].work();  // 体现了动态绑定机制
            // 通过类型的向下转型,进行运行类型的判断,判断运行类型是否为Phone
            if(usbs[i] instanceof Phone) {
                ((Phone) usbs[i]).call();
            }
        }
    }
}

// 声明接口
interface Usb {
    void work();
}
// 声明两个类去使用接口
class Phone implements Usb {
    @Override
    public void work() {
        System.out.println("手机工作中...");
    }
    
    public void call() {
        System.out.println("手机可以打电话...");
    }
}
class Camera implements Usb {
    @Override
    public void work() {
        System.out.println("相机工作中...");
    }
}
多态传递
java
package com.jlctest.interface;

public class InterfacePolyPass {
    public static void main(String[] args) {
        // 接口类型的变量 ig 可以指向 实现了该接口类的对象实例
        IG ig = new Teacher();   
        // 但是 IH ih = new Teacher();会报错,Teacher类没有实现IH接口  interface IG {}
        // 但是如果让IG接口去继承IH接口,进行多态传递  interface IG extends IH{}
        IH ih = new Teacher();  // 不报错
    }
}

// 声明接口
interface IH {}
// interface IG {}
interface IG extends IH{}

// 声明类去使用接口
class Teacher implements IG {}

具体来说:如果IG继承了IH接口,而Teacher类实现了IG接口,那么,实际上就相当于Teacher类也实现了IH接口


内部类

内部类是指,一个类的内部又完整的嵌套了另一个类结构。被嵌套的类被称为内部类(inner class),嵌套其他类的类称为外部类(outer class

基本语法:

java
class Outer {  // 外部类
    class Inner {  // 内部类
        ...
    }
}

class Other {  // 外部其他类
    ...
}

内部类是类的第五大成员,内部类最大的特点就是可以直接访问私有属性,并且可以体现类与类之间的包含关系,内部类是重点也是难点,底层源码使用了大量的内部类

java
class Outer {  // 外部类
    // 属性
    private int n1 = 100;  
    // 方法
    public void m1() {   
        System.out.println("m1()");
    }
    // 构造器
    public Outer(int n1) {
        this.n1 = n1;
    }
    // 代码块
    {
        System.out.println("代码块");
    }
    // 内部类
    class Inner {  
        ...
    }
}

内部类有四种,内部类的分类有两种:

  • 定义在外部类局部位置上(如方法体内):
    • 局部内部类(有类名)
    • 匿名内部类(没有类名,是一个重点)
  • 定义在外部类的成员位置上(属性和方法位置上):
    • 成员内部类(没有static修饰)
    • 静态内部类(使用static修饰)

局部内部类

局部内部类是定义在外部类的局部位置,通常在方法中,并且有类名

基本语法:

java
class Outer {  // 外部类
    // 属性
    private int n1 = 100;
    // 方法
    public void m1() {   
        // 定义局部内部类
        class Inner {
            // 可以直接访问外部类的所有成员,包含私有的(私有属性只能在本类进行访问)
            public void f1() {
                // 局部内部类访问外部类的成员方式是直接访问
                System.out.println(n1);    // 100
            }
        }
        // 同一作用域中,在外部类方法中,访问局部内部类的成员
        // 先创建局部内部类对象,再通过对象进行访问
        Inner inner = new Inner();
        inner.f1();
    }
}

注意事项:

  • 局部内部类,其本质还是一个类,类的要素都有

  • 局部内部类可以直接访问外部类的所有成员,包含私有的

  • 不能给局部内部类添加访问修饰符,因为它的地位就是一个局部变量,局部变量是不能使用修饰符的,但是可以使用final修饰,因为局部变量也可以使用final

  • 局部内部类的作用域:仅仅在定义它的方法体或代码块中

  • 局部内部类访问外部类的成员方式:直接访问(局部内部类-->访问-->外部类的成员)

  • 外部类访问局部内部类的成员方式:先创建对象再访问,同时必须要在作用域内

  • 外部其他类是不能访问局部内部类的(因为局部内部类的地位是一个局部变量)

  • 如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this.成员)去访问

    java
    class Outer {
        private int n1 = 100;
        public void m1() {   
            // 定义局部内部类
            class Inner {
                private int n1 = 200;
                public void f1() {
                    System.out.println(n1);    // 200   重名时,就近访问
                    // 如果一定要访问外部类,可以使用外部类名.this.成员的方式
                    // Outer.this本质为外部类的一个实例对象,即外界主函数中哪个对象调用了m1()方法,Outer.this就指向哪个对象
                    System.out.println(Outer.this.n1);  // 100
                }
            }
        }
    }

匿名内部类

匿名内部类在底层的框架和项目的开发中使用的非常多

匿名内部类是定义在外部类的局部位置,比如方法中,并且没有类名

基本语法:

java
new 类或接口(参数列表) {
    类体;
};
基于接口的匿名内部类
java
public class AnonymousInnerClass {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.method();
    }
}

class Outer {  // 外部类
    private int n1 = 10;
    public void method() {
        // 传统方式是写一个类,实现该接口,再创建这个对象,并指向这个接口
        // 如果需求是这个类只使用一次,那么定义的Tiger其他外部类就会浪费
        IA tiger = new Tiger();
        tiger.cry();    // 老虎叫...
        
        // 针对上述的情况,我们可以使用接口的匿名内部类进行简化,将外界的Tiger其他外部类去掉
        // tiger的编译类型是IA   运行类型是匿名内部类,底层为class XXX implements IA { ... }
        // 匿名内部类在底层中系统会分配一个类名的,XXX在底层为Outer$1 (外部类+$+数字)
        // jdk底层在创建匿名内部类Outer$1,立即就创建了Outer$1的实例,并且将地址返回给tiger
        // 匿名内部类使用一次,就不能再使用了,不是tiger对象,tiger对象可以一直使用
        IA tiger = new IA() {
            @Override
            public void cry() {
                System.out.println("老虎叫..."):
            }
        };
        tiger.cry();    // 老虎叫...
    }
}

interface IA {   // 接口
    public void cry();
}

class Tiger implements IA {
    @Override
    public void cry() {
        System.out.println("老虎叫..."):
    }
}
基于类的匿名内部类
java
public class AnonymousInnerClass {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.method();
    }
}

class Outer {  // 外部类
    private int n1 = 10;
    public void method() {
        // 创建一个Father的实例  编译类型和运行类似都是Father
        Father father = new Father("jack");
        
        // 基于类的匿名内部类
        // father的编译类型是Father  运行类型是匿名内部类(外部类+$+数字)
        // 运行类型是匿名内部类,底层为class XXX extends Father { ... }  有个继承的关系
        // 使用匿名内部类,同时也会返回匿名内部类的对象,将对象地址返回给father
        // 这里的参数列表,会传递给Father类写好的构造器
        Father father = new Father("jack") {
            @Override
            public void test() {
                System.out.println("匿名内部类重写了test方法");
                // 可以直接访问外部类的所有成员,包含私有的
                System.out.println(n1);   // 10
            }
        }
        fatehe.test();   // 匿名内部类重写了test方法
        
        // 基于抽象类的匿名内部类
        Animal animal = new Animal() {
            // 抽象类中下面内容是必须要写的
            @Override
            void eat() {
                System.out.println("小狗吃骨头");
            }
        }
        animal.eat();   // 小狗吃骨头
    }
}

class Father {
    // 构造器
    public Father(String name) {}
    public void test() {}
}

abstract class Animal {  // 抽象类
    abstract void eat();
}

注意事项:

  • 匿名内部类的本质也是一个类,是一个内部类(即定义在外部类的局部位置,比如方法中),且是没有类名的,同时匿名内部类还是一个对象

  • 匿名内部类既是一个类的定义,同时它本身也是一个对象,从语法上看,它既有定义类的特征,也有创建对象的特征

    java
    // 可以直接调用匿名内部类的方法    本质上就是对象.方法   匿名内部类本身也是返回对象
    new Father("jack") {
        @Override
        public void test() {
            System.out.println("匿名内部类重写了test方法");
        }
    }.test();    // 匿名内部类重写了test方法
  • 匿名内部类可以直接访问外部类的所有成员,包含私有的

  • 匿名内部类不能添加访问修饰符,因为它的地位就是一个局部变量

  • 匿名内部类的作用域:仅仅在定义它的方法或代码块中

  • 外部其他类不能访问匿名内部类(因为匿名内部类的地位是一个局部变量)

  • 如果外部类和匿名内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this.成员)去访问

匿名内部类的最佳实践

当做实参直接传递,简洁高效

java
public class AnonymousInnerClass {
    public static void main(String[] args) {
        // 使用匿名内部类当做实参直接传递,简洁高效
        f1(new IL() {
            @Override
            public void show() {
                System.out.println("这是一幅名画");
            }
        });
    }
    
    // 定义静态方法,形参是接口类型
    public static void f1(IL il) {
        il.show();
    }
}

interface IL {   // 接口
    void show();
}

如果不使用匿名内部类,我们就需要写出这个类去实现这个接口,通过硬编码的方式去实现

在匿名内部类中修改内容,只是影响这一个实参传入,如果使用硬编码的方式,修改了类中的内容,就会修改所有基于这个类实例出的对象

成员内部类

成员内部类是定义在外部类的成员位置,并且没有static修饰

java
class Outer {   // 外部类
    private int n1 = 10;
    
    // 定义成员内部类
    class Inner {
        public void say() {
            // 成员内部类中可以直接访问外部类中的所有成员
            System.out.println(n1);   // 10
        }
    }
    
    // 访问成员内部类中的方法,通过成员内部类实例化出一个对象,再使用这个对象进行调用
    public void t1() {
        Inner inner = new Inner();
        inner.say();
    }
}

注意事项:

  • 成员内部类可以直接访问外部类的所有成员,包括私有的

  • 成员内部类可以添加任意访问修饰符(publicprotected、默认、private),因为成员内部类的地位就是一个成员

  • 成员内部类的作用域和外部类的其他成员一样,为整个类体(成员内部类的类名可以在整个外部类中使用)

  • 外界类去访问成员内部类中的属性和方法,需要创建对象再调用

  • 外部其他类也可以访问成员内部类,有两种常见的方式:

    java
    public class AnonymousInnerClass {
        public static void main(String[] args) {
            Outer outer = new Outer();
            
            // 第一种方式
            // 相当于将new Inner()当作outer对象的一个成员
            Outer.Inner inner01 = outer.new Inner();
            inner01.say();
            
            // 第二种方式
            // 在外部类中,编写一个方法,可以返回Inner对象
            Outer.Inner inner02 = outer.getInnerInstance();
            inner02.say();
        }
    }
    
    class Outer {   // 外部类
        private int n1 = 10;
        // 定义成员内部类
        class Inner {
            public void say() {
                // 成员内部类中可以直接访问外部类中的所有成员
                System.out.println(n1);   // 10
            }
        }
        
        // 为外界返回一个Inner对象实例,供方法二调用
        public Inner getInnerInstance() {
            return new Inner();
        }
    }
  • 如果外部类和成员内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this.成员)去访问

静态内部类

静态内部类是定义在外部类的成员位置,并且有static修饰

java
public class AnonymousInnerClass {
    public static void main(String[] args) {
        Outer outer = new Outer();
        // 外部其他类访问静态内部类
        // 第一种方式
        // 静态内部类是可以通过类名直接访问的(前提是满足访问权限,私有的是不能访问的)
        Outer.Inner inner01 = new Outer.Inner();
        inner01.say();
        // 第二种方式
        // 在外部类中,编写一个方法,可以返回Inner对象实例
        Outer.Inner inner02 = outer.getInnerInstance();
        inner02.say();
        // 通过静态方法返回   静态方法是可以直接通过类去调用的
        Outer.Inner inner03 = Outer.getInner();
        inner03.say();
    }
}

class Outer {   // 外部类
    private int n1 = 10;
    private static String name = "jlc";
    // 定义静态内部类
    static class Inner {
        public void say() {
            // 静态内部类可以直接访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员
            System.out.println(n1);   // 报错
            System.out.println(name);  // jlc
        }
    }
    
    // 静态内部类的作用域:同其他的成员,为整个类体
    public void m1() {
        Inner inner = new Inner();
        inner.say();
    }
    
    // 为外界返回一个Inner对象实例,供方法二调用,通过非静态的方法
    public Inner getInnerInstance() {
        return new Inner();
    }
    // 也可以通过静态的方法,返回Inner对象实例
    public static Inner getInner() {
        return new Inner();
    }
}

注意事项:

  • 静态内部类可以直接访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员
  • 可以添加任意访问修饰符(publicprotected、默认、private),因为静态内部类的地位就是一个成员
  • 静态内部类的作用域:同其他的成员,为整个类体
  • 外部类访问静态内部类的成员,需要先创建对象,在调用访问
  • 如果外部类和静态内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this.成员)去访问

Released under the MIT License.