Java面对对象

635 阅读50分钟

初识对象

面向过程思想

  • 步骤清晰简单,第一步做什么,第二部做什么…
  • 面向过程适合处理一些较为简单的问题

面向对象思想

  • 物以类聚,分类的思维模式,思考问题首先会解决问题需要哪些分类,然后对这些分类进行单独思考。最后,才对某个分类下的细节进行面向过程的思索
  • 面向对象适合处理复杂的问题,适合处理需要多人协作的问题

特征:封装、继承、多态
过程:找对象、建立对象、使用对象、维护对象的关系
本质:以类的方式组织代码,以对象的组织(封装)数据

应用:对于描述复杂的事物,为了从宏观上把握、从整体上合理分析,需要使用面向对象来分析整个系统。但是,具体到微观操作,仍然需要面向过程的思路去处理。

类与对象的关系

是对某一类事物的共同描述,但并不能代表某一个具体事物,是 引用数据类型

对象 是现实世界中的具体事物
对象由对象属性和方法组成,它们共同定义了对象的特征和行为。
对象属性指的是对象所包含的数据,用于描述对象的状态和特征。

在Java中,类通过class关键字定义,对象通过new操作符创建

二者关系

类 是抽取了同类对象的共同属性(成员变量)和行为(成员方法)所定义的对象的“模板”
对象 是根据类的“模板”所创建的类的实例

创建与初始化

示例

public class MyClass {   //定义一个公共类MyClass  
    private int number;   // 定义一个私有整数型实例变量number 
      
    public MyClass(int num) { // 定义一个公共构造方法
        this.number = num;    //使用this来引用当前对象的number实例变量,并将传入参数num的值赋给它 
                              //构造方法 MyClass(int num) 被调用来初始化对象的 number 属性
    }  
      
    public void displayNumber() {  
        System.out.println("Number: " + number);  
    }  
}  
  
public class Main {  
    public static void main(String[] args) {  
        MyClass obj = new MyClass(10);  //创建了一个MyClass类的对象,并将其引用赋值给变量obj
        obj.displayNumber();  // 调用displayNumber方法来显示number的值  
    }  
}
  1. 创建对象

一旦类被定义,就可以在程序中通过使用 new 关键字来实例化该类,创建对象。
语法格式

ClassName objectName = new ClassName();
//ClassName是 要创建实例对象的 类的名称,objectName是创建的实例对象的名称(又叫引用变量名)
  1. 内存分配

在使用 new 关键字创建对象时,Java 在内存中为对象分配空间。对象通常被存储在堆内存中,而引用变量(如上面的 obj)则存储在栈内存中。栈内存中的引用变量是 指向堆内存中相应对象的地址,通过引用变量可以访问和修改该对象的属性和方法,但不是对象本身

  1. 构造方法的调用

在对象实例化的过程中,必须要调用构造方法(构造器)来完成对象的初始化工作,可以设置对象的初始状态或执行其他必要的操作。在上面的例子中,构造方法 MyClass(int num) 被调用来初始化对象的 number 属性。

拓展

在IntelliJ IDEA中,可以使用以下快捷键来快速生成构造方法:

  1. 按下Alt + Insert打开Generate菜单
  2. 选择Constructor选项并按回车键
  3. 在弹出的窗口中选择需要包含的字段,然后点击"OK"(有参)或"NO"(无参)

构造器

主要作用
在创建一个对象时进行初始化操作。通常用于设置对象的初始状态,包括初始化对象的成员变量、分配内存或执行其他必要的初始化任务。

本质:方法

构造方法的语法格式

public class ClassName {  
    public ClassName(parameters) {  // 构造方法,类名即方法名
        // 构造方法的实现,不写返回类型
    }  
}  

默认构造方法

如果一个类没有定义任何构造方法,编译器会自动为该类生成一个默认构造方法。这个默认构造方法没有参数不执行任何操作。当创建一个新的对象,且没有自定义构造方法时,Java虚拟机会自动调用这个默认构造方法,根据属性的类型为属性赋默认值(作用是为了方便程序员快速创建一个该类的对象,而不需要手动编写构造方法

默认构造方法的语法

public class ClassName {  
    public ClassName() {  // 默认构造方法  
                        // 不执行任何操作  
    }  
}

带参数的构造方法

调用一个带参数的构造方法时,编译器会检查传入的参数类型是否与带参构造器的参数列表中指定的所需参数相匹配。
如果参数类型不匹配,编译器会尝试进行类型转换以匹配传入的参数与构造器参数类型。 如果无法进行有效的类型转换,将会导致编译错误。
例如,如果构造器的参数列表中指定一个 int 的参数,但调用构造器时传入了一个 String 类型的参数,那么编译器将会报错:无法将字符串转换为整数。

因此

  • 在使用带参数的构造器时,确保传入的参数类型与带参构造器的参数列表中指定的所需参数相匹配。
  • 如果在类中定义了带参数的构造器,编译器将不再自动生成无参构造器。当需要创建一个没有传入参数的对象时,必须显式地定义一个无参构造器以供调用(除非通过反射)

this 关键字

  1. 作用:类的方法内部引用当前实例。当在一个构造方法或方法中引用一个实例变量时,需要使用 this 关键字来区分实例变量和局部变量

语法格式

this.实例变量 = 参数
语义:将参数值赋给类的实例变量,而不是方法内部的局部变量

在一个方法(如构造方法)内部,允许定义与 类变量 同名的局部变量(如此,在方法内部使用该变量名时将指向局部变量而不是类变量)。所以,如果直接使用变量名(如number = num;),Java会认为这是在引用方法内部的局部变量,而不是类的实例变量

为了避免这种混淆,Java提供了this关键字。当使用this.number = num;时,这意味着声明:“我要将这个num值赋给类的实例变量number,而不是方法内部的局部变量number”

为了进一步说明,示例:

public class MyClass {
    private int number;

    public MyClass(int number) {
        // ↓这里没有使用this关键字,Java会认为这在引用方法内部的局部变量
        // number = num;  // 这行会导致编译错误,因为此语句中number是方法内部的局部变量
        this.number = num;  // 正确的方式,将参数的值赋给实例变量
    }
}
  1. 作用:调用同一类中的另一个构造方法,且必须写在子类构造方法的第一行

示例

public class Person {  
    private String name;  
    private int age;  
  
  // ↓为Person对象提供多种创建方式,如果某人没有提供name或age,则使用默认值
    public Person() {  
        this("Unknown", 0);       //不为Person对象提供任何具体信息,创建一个默认的Person对象
    }  
  
    public Person(String name) {  //如果只知道某个人的名字,但不了解其年龄,可以使用这个构造方法
        this(name, 0);            //只为name提供一个值,而让age使用默认值
    }  
  
    public Person(int age) {      //如果只知道某个人的年龄,但不了解其名字,可以使用这个构造方法  
        this("Unknown", age);     //只为age提供一个值,而让name使用默认值
    }  
  
    public Person(String name, int age) {  
        this.name = name;  
        this.age = age;  
    }  
}

为对象属性赋值

为对象属性提供值通常可以通过以下几种方式实现:

  1. 创建属性时直接赋值
public class Person {
    private String name = "Jie"; 
    private int age = 19;
}
  1. 在构造方法中赋值:在对象的构造函数中,可以为对象的属性提供初始值。
public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = "Jie";
        this.age = 19;
    }
}
  1. 调用构造方法赋值:创建对象时,调用构造方法接受两个参数来为对象的属性赋值
Person Demo = new Person("Jie", 19);
  1. 使用setter方法:如果不希望在构造函数中为所有属性赋值,或者需要在对象创建后更改属性的值,你可以使用setter方法。
    2.1. 在创建属性的类中使用setter方法为属性赋值,示例:
public class Person {
    private String name;
    private int age;

    // Setter方法用于设置name属性的值
    public void setName(String newName) {
        this.name = newName;
    }

    // Setter方法用于设置age属性的值
    public void setAge(int newAge) {
        this.age = newAge;
    }
}
//setName和setAge就是两个setter方法,它们分别用于设置name属性和age属性的值

   2.2. 在其他类中使用setter方法为对象的属性赋值,示例:

public class Main {
    public static void main(String[] args) {
        Person person1 = new Person();
        
        // 使用setter方法为属性赋值
        person1.setName("Jie");
        person1.setAge(19);
    }
}

Lambda表达式

基础概念

定义:Lambda 表达式是一个可传递的匿名函数。我们可以从以下三个点来理解这个定义:

  • 匿名:它没有明确的名称,不像传统方法那样需要被声明在一个特定的类中。
  • 函数:它像方法一样,有参数列表、函数体、返回值类型,甚至还可能有抛出的异常列表。但它不像方法那样属于某个特定的类。
  • 可传递:Lambda 表达式可以作为参数传递给方法,或者赋值给一个变量。这是它最重要的特性,使得行为参数化变得非常简洁。

设计初衷:为了简化行为参数化的代码实现。在 Java 8 之前,传递行为的唯一方式是通过匿名内部类,代码非常臃肿。Lambda 的出现让Java也能进行简单的函数式编程。

语法结构

Lambda 表达式的基本语法如下:

(parameters) -> expression

(parameters) -> { 代码块; }

让我们来分解它的各个部分:

参数列表 (Parameters):

  • 类似于方法中的参数列表,放在括号 () 中。
  • 参数的类型可以显式声明,也可以由编译器根据上下文推断出来(通常可以省略类型)。
  • 如果只有一个参数且类型可推断,则括号 () 也可以省略。
  • 如果没有参数,则必须使用空括号 ()

箭头符号 ->:

  • 它将参数列表和Lambda主体分隔开。

Lambda 主体 (Body):

  • 可以是一个表达式,也可以是一个代码块
  • 表达式:如果 Lambda 体是一个单一的表达式(an expression;求值),而不是一个语句(statement;执行),则可以省略大括号 {}return 关键字。表达式的值就是Lambda的返回值。
    • (int a, int b) -> a + b //返回 a+b 的结果
  • 代码块:如果主体有多行语句,或者需要复杂的逻辑,则必须使用大括号 {},就像普通方法体一样。如果需要返回值,必须显式使用 return 语句。
    • (String s) -> { System.out.println(s); return s.length(); }

与匿名内部类的关系

  • 匿名内部类:Java 8 之前的主要写法
  • Lambda 表达式:Java 8 引入的新特性(简化写法)

只有函数式接口(只有一个抽象方法的接口)才能用Lambda 表达式写

转换规则:

Lambda 表达式:(参数) -> {方法体}
等价于
匿名内部类:
new 接口名() {
    @Override
    public 返回类型 方法名(参数) {
        // 方法体
    }
}

区别

特性匿名内部类Lambda 表达式
适用范围任何类/接口只能函数式接口
属性定义可以不可以
方法定义可以多个只有一个(实现抽象方法)
构造函数可以有(隐式)不可以
初始化块可以有不可以
代码简洁性较冗长很简洁
this 含义指向匿名内部类实例指向外部类实例

函数式接口:Lambda的类型是什么?

Lambda 表达式的类型在 Java 中就是 「函数式接口」

函数式接口的定义:一个仅包含一个抽象方法的接口。(注意:它可以包含多个默认方法或静态方法,但只要抽象方法只有一个即可)。

@FunctionalInterface 注解

  • 这是一个可选的注解,用于标记一个接口是函数式接口。
  • 作用:编译器检查。如果你给一个接口加上了这个注解,但接口里却有多个抽象方法,编译器就会报错。这是一种保护机制。

为什么需要函数式接口?

在 Java 的强类型体系下,函数式接口为原本“无类型”的 Lambda 表达式提供了一个具体的、编译器可识别和检查的类型。没有这个类型,Lambda 表达式无法被定义、传递和编译。

示例理解:将 Lambda 表达式赋值给一个 函数式接口类型 的变量。

// Lambda 表达式 -> (String s) -> System.out.println(s)
// 函数式接口类型 -> Consumer<String>,<String>表示该接口用于处理字符串输入。
Consumer<String> printer = (String s) -> System.out.println(s);

这个赋值动作发生了两件重要的事:

  1. 类型推断:编译器根据左侧变量的类型 (Consumer<String>),反过来推断出右侧 Lambda 表达式的参数类型和返回类型。这就是“上下文推断”。
  2. 行为实现:Lambda 表达式 (String s) -> System.out.println(s) 的内容,成为了 Consumer<String> 接口中那个唯一的抽象方法 void accept(String t) 的具体实现。

于是,这个Lambda 表达式就“获得”了一个类型:Consumer<String>  它现在是一个 Consumer<String> 类型的对象,可以像任何其他对象一样被传递和使用。

经典示例:Runnable 接口

// 1. 使用匿名内部类
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello World 1");
    }
};

// 2. 使用 Lambda 表达式
Runnable r2 = () -> System.out.println("Hello World 2");

// 执行
r1.run();
r2.run();

Runnable 接口只有一个抽象方法 run(),所以它是一个函数式接口。Lambda () -> ... 就成为了 run() 方法的实现。

Java 8 内置的核心函数式接口

Java 8 在 java.util.function 包中引入了许多常用的函数式接口,无需自己创建。最常见的有以下四大类:

1. Function<T, R>:函数型接口

  • 抽象方法R apply(T t)
  • 描述:接收一个 T 类型的参数,返回一个 R 类型的结果。
  • 用途:将输入值映射/转换为另一种类型的输出值。
  • 示例:将字符串转换为它的长度。
    Function<String, Integer> lengthFunction = (String s) -> s.length();
    Integer length = lengthFunction.apply("Hello"); // 返回 5
    

2. Predicate<T>:断定型接口

  • 抽象方法boolean test(T t)
  • 描述:接收一个参数 T,返回一个布尔值。
  • 用途:用于判断是否满足某种条件。
  • 示例:判断字符串是否为空。
    Predicate<String> isEmptyPredicate = (String s) -> s != null && !s.isEmpty();
    boolean result = isEmptyPredicate.test(""); // 返回 false
    

3. Consumer<T>:消费型接口

  • 抽象方法void accept(T t)
  • 描述:接收一个参数 T,不返回任何结果。
  • 用途:用于执行一些操作,比如打印、修改对象等。
  • 示例:打印字符串。
    Consumer<String> printer = (String s) -> System.out.println(s);
    printer.accept("Hello Lambda!"); // 输出 Hello Lambda!
    

4. Supplier<T>:供给型接口

  • 抽象方法T get()
  • 描述:不接收任何参数,返回一个 T 类型的结果。
  • 用途:用于“提供”或“生成”一个对象,例如工厂方法。
  • 示例:生成一个随机数。
    Supplier<Double> randomSupplier = () -> Math.random();
    Double randomValue = randomSupplier.get();
    

其他常见接口UnaryOperator<T>(一元操作,继承自 Function<T, T>),BiFunction<T, U, R>(两个参数的Function)等。

方法引用与构造器引用:语法糖

当 Lambda 体仅仅是调用一个已存在的方法时,可以使用方法引用,让代码更简洁。

语法目标对象::方法名

主要有三种情况:

  1. 对象::实例方法

    • 要求:Lambda 的参数列表与实例方法的参数列表一致。
    • (args) -> object.instanceMethod(args) 等价于 object::instanceMethod
    • System.out::println 等价于 (s) -> System.out.println(s)
  2. 类::静态方法

    • 要求:Lambda 的参数列表与静态方法的参数列表一致。
    • (args) -> Class.staticMethod(args) 等价于 Class::staticMethod
    • Math::max 等价于 (a, b) -> Math.max(a, b)
  3. 类::new (构造器引用)

    • 要求:用于创建对象。
    • () -> new Class() 等价于 Class::new
    • Supplier<List<String>> listSupplier = ArrayList::new; // 等价于 () -> new ArrayList<>()

变量作用域与访问限制

Lambda 表达式可以访问其外部作用域的变量,但有重要限制:

  • 捕获局部变量:Lambda 可以捕获(使用)其所在外部作用域内的局部变量
  • 限制:必须为 effectively final
    • 这些被捕获的局部变量必须是 effectively final 的,这意味着这些变量在初始化后不允许再被重新赋值(无论是否显式声明为 final
    • 任何尝试修改 Lambda 捕获(使用)的这些变量会导致编译错误
    • 例外情况:实例变量和静态变量可以被捕获并且可以修改(因为它们存储在堆中,其访问由JVM管理;但需注意线程安全)。
  • 原因:出于并发和安全性的考虑。局部变量保存在栈上,而Lambda可能在另一个线程中使用。为了避免不同步的问题,强制要求这些变量不可变。

总结:优点、最佳实践与简化示例

优点

  1. 代码简洁:极大减少了匿名内部类的模板代码。
  2. 行为参数化:轻松地传递代码(行为),使API更强大、更灵活。
  3. 并行处理能力:与 Stream API 结合,可以非常方便地编写出高效的多线程并行处理代码。

最佳实践

  • 保持简短:Lambda 表达式应该是一目了然的,如果逻辑复杂,超过几行,考虑将其提取为一个命名方法,然后使用方法引用。
  • 避免副作用:尽量编写纯函数式的Lambda(输出完全由输入决定,不修改外部状态),这更利于并行化和推理。
  • 使用 @FunctionalInterface 注解:如果你在自定义函数式接口,请加上它。

简化示例

// 从内部类到Lambda表达式的代码优化

// 基本逻辑:
public class Demo9_Lamda {
    public static void main(String[] args) {
        ILike like = new Like();
        like.lamda();
    }
}
interface ILike {  // 1.定义一个函数式接口
    void lamda();
}
class Like implements ILike {  // 2.实现类
    @Override
    public void lamda() {
        System.out.println("I like lamda");
    }
}

// 优化逻辑:
public class Demo_Lamda1 {
//优化一:静态内部类
    static class Like1 implements ILike {
        @Override
        public void lamda() {
            System.out.println("I like lamda1");
        }
    }
    public static void main(String[] args) {
        ILike like = new Like1();
        like.lamda();
    }
//优化二:局部内部类
    public static void main(String[] args) {
        class Like12 implements ILike {
            @Override
            public void lamda() {
                System.out.println("I like lamda2");
            }
        }
        ILike like = new Like12();
        like.lamda();
    }
//优化三:匿名内部类
    public static void main(String[] args) {
        ILike like = new ILike () {
            @Override
            public void lamda() {
                System.out.println("I like lamda3");
            }
        };
        like.lamda();
    }
    
//最终优化:Lambda表达式
    public static void main(String[] args) {
        ILike like = () ->{
            System.out.println("I like lamda4");
        };
        like.lamda();
    }
}

内存分析

代码

//定义将实例化的类
public class Pet {
    public String name = "小黑"; //默认 null
    public int age = 4; 	//默认 0
    //无参构造

     public Pet(String name, int age) {  
        this.age = age;  
        this.name = name;  
    }  
    
    public void shout(){
        System.out.println("叫了一声");
    }
}
//定义测试类,创建调用对象
public class Application {
    public static void main(String[] args) {
        
        Pet dog = new Pet("旺财",3);
        dog.shout();
    }
}

内存图解

  1. 加载含main类的字节码文件:因为main() 函数是程序的唯一入口,因此,包含main函数的Application类的字节码文件会优先加载到方法区。
  2. main函数进栈
  3. 执行main中代码:开始执行main方法的第一行代码,从右向左执行,首先便遇到了new关键字,因为new关键字后面出现了Pet类,表示要实例化的是Pet类,而Pet类的字节码文件还没有加载到方法区,JVM 不认识Pet,因此下一步是将Pet类的字节码文件加载到方法区(假设Pet类成员方法在方法区中的地址值为0x0003)
  4. new关键字开始:Pet类的字节码文件加载到方法区之后,new关键字会在堆内存开辟空间给新的Pet类对象,假设Pet类对象在内存中的地址值为0x0001。
  5. 在堆中开辟空间:根据Pet类的字节码文件,在堆中分配的这块内存空间会被分成三部分,分别是属性值name,age,和Pet类的成员方法,Pet类的成员方法部分其实是成员方法在方法区中的地址值,将来如果对象调用成员方法,可以通过这个地址值找到方法区中的成员方法。
  6. 默认初始化:对 对象的属性值进行第一次初始化:默认初始化。对应于Pet类中,name是String类型默认值null;age是int类型默认值0。
  7. 显式初始化:可以在定义Pet类时,对name和age分别赋了初始值,进行第二次初始化:显式初始化。(如果没有在类中对属性赋初始值,则没有这一步)
  8. 构造器初始化:创建一个对象,需要构造器来进行构造器初始化,如果调用的是Pet类的带参构造器,那么要进行第三次初始化 : 构造器初始化。即将属性值更改为调用带参构造时传入的形参。
  9. 常量池:这里的字符串常量"旺财",在常量池中,有自己的地址,调用模式类似于成员方法。name属性这里其实保存的是"旺财"在常量池中的地址值
  10. new关键字结束:最后,把对象的地址值返回给dog引用就可以了
未命名文件.png

再创建一个对象Pet cat = new Pet(); 屏幕截图 2024-01-27 001742.png

封装

程序设计要追求“高内聚,低耦合”:高内聚就是类的内部数据细节由自己完成,不允许外部干涉;低耦合:仅暴露少量的方法给外部使用。

封装就是将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法(getter和setter)来实现对隐藏信息的操作和访问。

作用

  • 提高程序的安全性,保护数据。例如限制给对象赋值的范围(避免数据类型的不同或者数据范围超出预计)
  • 隐藏代码的实现细节
  • 统一接口
  • 增加系统的可维护性

步骤

  1. 使用关键字private对类的属性进行隐藏(方法一般不用隐藏)
  2. 利用getter/setter方法对属性值进行读写
  3. 可以在方法中加入属性控制语句,对属性值的合法性进行判断

示例

public class Test {
    public static void main(String[] args) {
        Person student = new Person();   //创建一个Person类的实例,并将其引用赋值给student变量
        student.setName("newName");   //调用student对象的setName方法,并为其设置一个名字"newName"
        System.out.println(student.getName());   //调用student对象的getName方法来获取其名字
        student.setAge(999);  //不合法
        System.out.println(student.getAge());
    }
}

public class Person{
    private String name;   // 私有属性
    private int age;
    // ↓提供一些方法来操作私有属性,一般会在里面加入一些安全性的判定
    public String getName(){   //供外部调用以获得这个数据
        return this.name;
    }
    public void setName(String name){   //供外部调用以给这个数据赋值
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        if (age<120 && age>0){  //检查尝试为age赋的值是否在0到120的范围内
            this.age = age;
        }else{
            System.out.println("年龄必须在0到120之间。");
    }
}

快捷键

自动生成 getter 和 setter 方法
按下 Alt + Insert,在弹出的菜单中选择 "Getter and Setter",再选择要生成方法的属性,最后按 Enter

访问限定符

修饰符同类中同包中不同包子类中不同包非子类中
public✅ 可访问✅ 可访问✅ 可访问✅ 可访问
protected✅ 可访问✅ 可访问✅ 可访问❌ 不可访问
默认(无修饰符)✅ 可访问✅ 可访问❌ 不可访问❌ 不可访问
private✅ 可访问❌ 不可访问❌ 不可访问❌ 不可访问

说明

  • 同类中:成员定义所在的类(定义类)内部。
  • 同包中:与定义类位于同一包下的其他类。
  • 不同包子类:继承定义类的子类,但位于其他包中。
  • 不同包非子类:与定义类无继承关系且位于其他包中的类。

继承

继承:子类(派生类)继承 父类(基类)的特征和行为

作用:使得子类对象(实例)具有父类的成员变量和方法,并且可以通过添加新的成员变量和方法来扩展其功能,快速创建新的类。(实现代码的重用,提高程序的可维护性,节省大量创建新类的时间,提高开发效率和开发质量。)

Protected:受保护的访问权限修饰符,用于修饰属性和方法;专门用于子类继承父类所使用的修饰符;不能被其他包内的非子类访问

extends 申明继承关系

作用:申明继承关系

语法格式

public class SubClass extends SuperClass {
    // 添加自己的成员变量和方法
}
//SubClass是子类,SuperClass是父类,通过extends关键字,SubClass继承了SuperClass的属性和方法,并且可以在其基础上添加自己的成员变量和方法
  • Java中一个类只能继承一个父类
  • 继承是类与类之间的一种关系,此外还有依赖、组合、聚合等
  • 继承表示一种分类关系——is a关系:子类代表了更特殊的类型,而父类代表了更一般的类型(例如:如果有一个 Vehicle 父类和 Car 子类,那么可以说“汽车是一种交通工具”,符合“is a”的关系)
  • 父类的private私有属性及方法无法被继承
  • 所有类都默认直接或间接继承Object类 (Ctrl+H 可以查看类关系)
  • 被final修饰的类,无法被继承

示例

class Person{                //声明一个Person类为父类
    protected String name;             //定义父类的成员变量name、age   
    protected int age;
    void show(){             //定义父类成员方法,将成员变量输出
        System.out.println(name); 	  
        System.out.println(age); 
    }
}
class Student extends Person {     //声明一个Student类为子类并继承父类
    int grade = 13;                //定义一个新的成员变量
}
public class myfirst {
    public static void main(String[] args) {
    System.out.println("学生信息如下:");
    Student student=new Student();     //声明一个Student类的实例对象student
    student.name="Jie";                //子类调用父类的成员变量name并赋值
    student.age=19;                    //子类调用父类的成员变量age并赋值
    student.show();                    //子类调用父类的成员方法show
    System.out.println(student.grade);
    }
}

//输出结果
学生信息如下:
Jie
19
13

Super 调用父类的构造方法

作用:调用父类的构造方法,且必须写在子类构造方法的第一行

  • super能且只能出现在子类的方法或构造方法中
  • 子类是不继承父类的构造方法的,它只是调用(隐式或显式),且默认会先调用父类的无参构造方法。

语法格式

super.属性名        //访问父类的属性
super.方法名(实参)  //访问父类的方法
super(实参)         //调用父类的构造方法

如果父类构造器没有参数,则在子类的构造器中不需要使用 super 关键字调用父类构造器,系统会自动调用父类的无参构造器;
如果父类的构造器带有参数,则必须在子类的构造器中显式地通过 super 关键字调用父类的构造器并配以对应的参数列表;

  • 使用无参数的super()方法,只能调用父类的无参数构造方法;
  • 使用带有对应参数列表的super()方法,只能调用父类的有对应参数列表的构造方法。

super()和this()
二者不能同时调用构造方法,因为this也必须写在第一行

super与this的区别:super代表父类对象的引用,只能在继承条件下使用;this调用自身对象,没有继承也可以使用

super()用于调用父类的构造方法

示例

class Parent {
    private int value;
    protected Parent() {
        value = 10;   // 初始化对象的逻辑
        System.out.println("Parent constructor");
    }
    public int getValue() {    // 添加一个公共方法来获取value的值
        return value;
    }
}

class Child extends Parent {
    Child() {
        super();   // 调用父类构造方法
        System.out.println("Child constructor");
    }   // 添加一个方法来打印value,但这并不是必需的,因为可以直接从外部调用getValue()
    public void printValue() {
        System.out.println("Value: " + getValue());
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child(); // 创建Child对象
        child.printValue(); // 调用Child类的printValue方法来打印value
        // 或者直接调用getValue()并打印
        // System.out.println("Value: " + child.getValue());
    }
}

//输出结果
Parent constructor
Child constructor 
Value: 10

父类和子类中有共同的属性特征和方法,使用super在子类中调用父类和子类中共同的属性特征和方法时,super.不能省略

示例

class Parent {
    protected String sharedAttribute = "shared";

    public void display() {
        System.out.println("This is a method in the parent class");
    }
}

class Child extends Parent {
    protected String sharedAttribute = "child's attribute";

    public void display() {
        super.display(); // 调用父类的方法
        System.out.println(super.sharedAttribute); // 访问父类中的属性
        System.out.println("This is a method in the child class");
        System.out.println(this.sharedAttribute); // 访问子类中的属性
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
        child.display();
    }
}

//输出结果
This is a method in the parent class  
shared  
This is a method in the child class  
child's attribute

方法重写

方法重写:在子类中重新定义父类中同名的实例方法,二者具有相同的签名(名称、参数列表)和返回值(父类中的实例方法被重写)

  • 重写是方法的重写,与属性无关
  • 静态方法属于类,非静态方法属于对象
  • 被static(属于类,不属于实例),final(常量方法),private(私有)修饰的方法不能重写
  • 在子类中重写的方法,其访问权限不允许小于父类中被重写的方法
class Vehicle {  // 父类(超类)       
    public void move() {  // 定义一个move()方法 
        System.out.println("The vehicle moves");  
    }  
}  
  
class Car extends Vehicle {  // 子类(派生类)      
    @Override  // 表示下面的方法是重写父类的move()方法  
    public void move() {  
        System.out.println("The car drives");  
    }  
}  
  
public class Main {  
    public static void main(String[] args) {  
        Car myCar = new Car();  // 创建一个Car对象
        myCar.move();   // 调用重写的move()方法  
    }  
}
// 输出: "The car drives"

@Override

@Override是一个注解,用来表明某个方法是重写(覆盖)了父类或接口中的方法。使用@Override注解能够帮助开发者准确地标识出子类或实现类中的方法是对父类或接口中的方法进行了重写,同时也能够提供一些编译时的检查,确保方法签名和父类或接口中的方法一致。

快捷键

快速生成重写父类方法的代码 按下 Alt + Insert,在弹出的菜单中选择 "override"

方法隐藏

方法隐藏:在子类中重新定义父类中同名的静态方法,二者具有相同的签名(名称、参数列表)和返回值(父类中的静态方法被隐藏)

这意味着如果通过父类引用调用该方法,则会调用父类中的方法;而如果通过子类引用调用该方法,则会调用子类中的方法。

在子类中隐藏的方法,其访问权限不允许小于父类中被隐藏的方法

示例

class Parent {
    public static void staticMethod() {
        System.out.println("Parent's static method");
    }
}
class Child extends Parent {
    public static void staticMethod() {
        System.out.println("Child's static method");
    }
}
public class Main {
    public static void main(String[] args) {
        Parent.staticMethod(); // 调用父类的静态方法
        Child.staticMethod(); // 调用子类的静态方法
    }
}
//输出结果
Parent's static method
Child's static method

多态

多态:在父类中定义的方法被子类继承后,让一个父类的引用变量指向其子类的对象,从而根据实际执行的对象类型来调用相应的方法。这使得同一个属性或方法在父类及其各个子类中具有不同的含义

原理

  • 一个对象的实际类型是唯一的,但可以指向对象的引用可以有很多
  • 一个对象的编译类型与运行类型可以不一致
  • 实际类型在定义对象时,就确定了,不能改变,而引用类型是可以变化的

分类:分为编译时多态和运行时多态

  • 编译时多态:指方法的重载,它是根据参数列表的不同来区分不同的方法
  • 运行时多态:通过动态绑定实现,是动态的

存在的必要条件,缺一不可:

  • 有继承关系
  • 子类重写父类方法
  • 继承的基础上,父类引用指向子类对象

注意点:

  • 多态是方法的多态,没有属性的多态
  • 父类和子类,有联系类型转换异常: ClassCastException
  • 在定义对象时判断类型:编译类型在 = 号的左边,运行类型在 = 号的右边

示例

class Instructor {  
    public void giveLesson() {  
        System.out.println("Teaching lesson");  
    }  
}  
  
class Student extends Instructor {   //Student继承Instructor类 
    @Override
    public void giveLesson() {   //重写giveLesson方法
        System.out.println("Learning lesson");  
    }  
}  
  
public class Main {  
    public static void main(String[] args) {  
     // ↓创建一个新的Student对象,并将其引用赋值给名为instructor1的变量,该变量的类型是Instructor
        Instructor instructor1 = new Student();   
        Instructor instructor2 = new Instructor();            
        instructor1.giveLesson(); //instructor1引用的是Student对象,而Student类重写了giveLesson方法
                                  //所以当调用这个方法时,打印出"Learning lesson"
        instructor2.giveLesson(); //instructor2引用的是原始的Instructor对象
                                  //所以当调用这个方法时,打印出"Teaching lesson"
    }  
}
//当调用giveLesson方法时,会根据实际执行的对象类型来确定到底调用哪个类的 giveLesson方法,这是多态的体现

instanceof 关键字

作用:判断一个对象是否是一个特定类的实例或其子类的实例,将会得到一个boolean值结果

语法格式

object instanceof class
//object是要检查的对象;class是一个已定义的类或接口

动态绑定

基于对象的实际类型以确定要调用的方法,而不是基于对象的引用类型
基于对象的引用类型以确定要访问的属性,而不是基于对象的实际类型

  • 对象的多态性体现在方法上,而不是属性上

示例

class Animal {  
    public String name = "Animal";  
    public void makeSound() {  
        System.out.println("The animal makes a sound");  
    }  
}  
  
class Dog extends Animal {  
    public String name = "Dog";  
    @Override  
    public void makeSound() {  
        System.out.println("The dog barks");  
    }  
}  
  
public class Main {  
    public static void main(String[] args) {  
        Animal animal = new Dog(); // 向上转型:将Dog对象引用赋给Animal类型变量
        animal.makeSound(); // 动态绑定:JVM会根据实际对象类型来决定调用哪个实现类的方法
        System.out.println(animal.name); // 输出 "Animal",而不是 "Dog"  
    }  
}
//输出
The dog barks 
Animal

类型转换

多态的转型:指通过向上转型和向下转型来实现对象的类型转换。

向上转型

意义:父类类型的引用指向子类对象

特点:

  • 可以调用父类的所有成员(需遵守访问权限)
  • 不能调用子类的特有成员
  • 运行结果由子类方法的方法体决定

语法格式

父类类型 引用名 = new 子类类型();
//右侧创建一个子类对象,把它当作父类看待使用

向下转型

意义:将一个已经进行了向上转型的对象(即被父类类型的引用指向的子类对象),重新转换回子类类型

  • 当向下转型后,就可以调用子类类型中所有的成员
子类类型 引用名 = (子类类型) 父类引用;
//用强制类型转换的格式,将父类引用类型转为子类引用类型

多态综合示例

class Animal {  
    public void makeSound() {  
        System.out.println("The animal makes a sound");  
    }  
}  
    
class Dog extends Animal {  
    @Override  
    public void makeSound() {  
        System.out.println("The dog barks");  
    }  
  
    public void wagTail() {  
        System.out.println("The dog wags its tail");  
    }  
}  
  
public class PolymorphismExample {  
    public static void main(String[] args) {  
        Animal myAnimal = new Dog();    //向上类型转换:将子类的实例赋值给父类的引用变量
        myAnimal.makeSound();           //调用makeSound方法时会使用动态绑定,输出 "The dog barks"  
        if (myAnimal instanceof Dog) {  //使用instanceof检查类型  
            Dog myDog = (Dog) myAnimal; //向下类型转换
            myDog.wagTail();            //输出 "The dog wags its tail"  
        }  
    }  
}
//输出结果
The dog barks 
The dog wags its tail

static详解

  1. 声明与初始化
  • 静态变量使用static关键字声明。
  • 静态变量可以在声明时初始化,也可以在类中的其他地方(如方法或初始化块)进行初始化。
  1. 访问
  • 可以通过类名直接访问静态变量,不需要创建类的实例,也称类变量
  1. 内存分析
  • 静态区代码(类中包含的静态变量和静态方法)在类加载时被分配内存并初始化。当 JVM 第一次实例化该类(使用 new 关键字创建对象)时,静态区代码会最早执行,并且只会执行一次。因此,所有该类的实例共享相同的静态变量和方法,它们不会随着每个实例的创建而分配新的内存空间或进行额外的初始化,对静态变量的修改将影响到所有实例。

静态代码块
使用关键字static进行声明
语法格式

static {  
        // 在这里编写需要在类加载时执行的代码  
}

静态导入

一种特殊的导入方式,允许程序员直接使用类或接口的静态成员(如静态变量、静态方法等),而无需使用完全限定的类名(包括包名和类名)。可以简化代码并提高可读性。 (例如:java.lang.String是一个完全限定的类名,其中java.lang是包名,String是类名。)

  1. 导入单个静态成员
    语法格式
import static [导入类的完全限定名].[导入的静态成员名]
import static java.lang.Math.PI;   //导入java.lang.Math类中的PI静态变量

之后,可以在代码中直接使用静态成员名导入静态成员

  1. 导入类中的所有静态成员
    语法格式
import static  [导入类的完全限定名].*  //使用星号(*)通配符
import static java.lang.Math.*;   //这将导入Math类中的所有静态成员

示例

//静态导入包
import static java.lang.Math.random;

public class Application {
    public static void main(String[] args) {
        //第一个随机数,不使用导包
        System.out.println(Math.random()); //0.7562202902634543
        //第二个随机数,使用静态导入包
        System.out.println(random()); //0.5391606223844663
    }
}

抽象

抽象方法 (abstract)

  • 抽象方法能存在于抽象类、接口及枚举类中
  • 抽象方法声明的完整结构
    [访问修饰符] abstract 返回值类型 方法名(参数列表);  
    

定义和实现抽象方法时,允许的访问修饰符

  1. 定义时
    • 抽象类:三选一(public/protected/默认包私有)
    • 接口:唯一选择(public)
  2. 实现时
    • 权限只能不变或放宽(public > protected > 默认包私有)
    • 接口实现必须public

    枚举类中抽象方法的访问修饰符规则与抽象类相同。

抽象类(abstract)

  • 外表形式:使用 abstract 关键字修饰的类(abstract class 类名 {}

  • 内部组成:至少包含一个抽象方法(无实现),可以包含具体方法(有实现)、变量和常量。(无抽象方法的抽象类极少使用)

  • 特性

    • 抽象类不能被实例化,继承并实现抽象类中全部抽象方法的子类可以被实例化。
    • 抽象类的被声明为具体类的子类必须实现抽象类中的全部抽象方法,这样的子类可以被实例化。
    • 抽象类的被声明为抽象类的子类必须不实现或只实现部分抽象类中的抽象方法,可以添加新的抽象方法,这样的子类不可以被实例化,须由子类的子类实现其全部抽象方法,再实例化“孙子”类。
  • 用途用法
    需求情况

    • 当多个类需要具备一个/些相同意图(即相同的方法头)但实现这个/些相同意图的具体操作(方法体)不同时。
    • 通常实现同一抽象类的这些类属于同一家族,他们有共同的核心状态(字段)、共同的行为(方法)和共同的生命周期。

    代码行为:创建一个抽象类作为父类,在其中除了定义公用属性和公用方法,还要定义一个/些供子类实现这个相同意图的方法头(即抽象方法),子类继承后,必须实现抽象类中的全部抽象方法,再各自“因类制宜”实现这个/些相同意图。
    子类实现父类的抽象方法的操作

    1. 去掉抽象方法的abstract关键字,补上方法体大括号。
    2. 个性化实现父类抽象方法的具体逻辑代码。
  • 意义:强制子类实现核心功能,促进代码复用。

抽象类使用案例

// 抽象类:图形
abstract class Shape {
    // 公共属性
    protected String color;
    // 公共方法(有具体实现)
    public void setColor(String color) {
        this.color = color;
        System.out.println("设置图形颜色为: " + color);
    }
    // 抽象方法(要求子类实现)
    public abstract double calculateArea();
}

// 具体子类:圆形
class Circle extends Shape {
    private double radius;
    // 类的构造方法
    public Circle(double radius) {
        this.radius = radius;  //创建对象时,将传入的参数赋值给当前对象的 `radius` 属性
    }
    // 实现抽象方法(因类制宜)
    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
    // 特有方法
    public void printRadius() {
        System.out.println("圆形半径: " + radius);
    }
}

// 具体子类:矩形
class Rectangle extends Shape {
    private double width;
    private double height;
    // 类的构造方法
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;  //创建对象时,将传入的参数赋值给当前对象的 `width` `height`属性
    }
    // 实现抽象方法(因类制宜)
    @Override
    public double calculateArea() {
        return width * height;
    }
}

// 抽象子类:三角形(未实现抽象方法)
abstract class Triangle extends Shape {
    // 没有实现抽象方法calculateArea(),所以必须声明为抽象类
    // 但可以有其他具体方法
    public void triangleFact() {
        System.out.println("我有三个角");
    }
}

public class Main {
    public static void main(String[] args) {
        // 使用圆形
        Circle circle = new Circle(5.0);
        circle.setColor("红色");  // 继承的公共方法
        System.out.println("圆形面积: " + circle.calculateArea());
        circle.printRadius();
        // 使用矩形
        Rectangle rect = new Rectangle(4.0, 6.0);
        rect.setColor("蓝色");  // 继承的公共方法
        System.out.println("矩形面积: " + rect.calculateArea());

        // 抽象子类不能实例化
        // Triangle tri = new Triangle();  // 编译错误
    }
}

代码输出

设置图形颜色为: 红色
圆形面积: 78.53981633974483
圆形半径: 5.0
设置图形颜色为: 蓝色
矩形面积: 24.0

接口

组成

外表形式:使用 interface 关键字定义。

接口与类平行存在,不是类的特殊形式。

  • 权限修饰符

    public修饰符:public修饰的接口可以在同一项目(实际上是同一模块或类路径)的任何包中的类或接口访问。

    • 模块化、传统项目的区别

      场景传统项目(类路径)模块化项目
      public接口访问全项目随便用需同时满足:① 对方模块exports包;② 本模块requires对方
      包私有接口访问仅同包可用仅同包可用
      适用Java版本Java 1.0-8Java 9+
    • 文件名规则:一个 .java 源文件里只能有一个 public 的顶级接口(或类),该文件的命名必须与这个public顶级接口(或类)的命名相同

      与之相对的是,内部类接口(或类)不受此文件名规则限制

    默认接口(无修饰符):这种接口只能被同一个包内的类或接口访问。

内部组成

  • 变量

    • 接口中没有成员变量
    • 接口中任何一个定义的量都隐式修饰为 public static final(即必为常量),且必须在接口中完成初始化
    • 所有接口方法均可直接访问接口常量
  • 成员方法:下述方法都是可选的。接口可以完全为空(如标记接口 Serializable

    1. 抽象方法:定义必须由实现类实现的相同意图(相同的方法头)(默认public abstract可省略)
    • 被调用:任何能访问 接口及其实现类 的类(包括非实现类和实现类),都可以通过实现类的实例实现类的实例名.抽象方法名()调用接口中的抽象方法
    • 可调用:默认方法、静态方法(接口名.静态方法名()
    • 用途:将可变核心逻辑声明为抽象方法
    1. 默认方法:是使用default修饰的完整方法(默认public可省略)
    • 性质:实现类可以重写默认方法(可选的;继承的多个接口中存在相同签名冲突时必须重写)
    • 被调用:
      • 任何能访问 接口及其实现类 的类(包括非实现类和实现类),都可以通过实现类的实例实现类的实例名.默认方法名()调用接口中的抽象方法
      • 子接口重写父接口的默认方法时,通过 父接口.super.重写的默认方法名() 调用
    • 可调用:
      • 其他任何接口方法(抽象方法实际调用的逻辑是实现类的实现)
    • 用途:将稳定通用的逻辑放在默认方法中
    1. 静态方法:是使用static修饰的完整方法(默认public可省略)
    • 性质:不继承给子接口或实现类,也不能通过实现类的 实例名.静态方法名() 调用,不可重写
    • 被调用:任何能访问该接口的类(包括实现类、非实现类),都可以通过 接口名.静态方法名() 调用接口的静态方法。
    • 可调用:其他任何静态方法(包括私有)、默认方法(需创建接口实例引用,罕见且不建议)
    • 用途:为接口的其他方法(默认方法)和实现类提供通用的、无状态的工具函数
    1. 私有方法:是使用private修饰的完整方法(Java 9+引入)
    • 可调用:仅能在接口内被调用,根据自身类型遵循默认方法或静态方法的调用规则

    • 用途:实现接口内部代码复用,保持公共方法简洁

    接口内部,所有方法间的调用一般直接使用 方法名()

  • 实践协同:

    1. 底部是少量抽象方法(核心契约)
    2. 中层是默认方法(构建增强功能)
    3. 顶部是静态方法(提供工具支持)
    4. 私有方法贯穿各层实现代码复用

特性

  • 类可以实现接口:class 类名 implements 接口名 {...实现逻辑...}
  • 具体类实现接口,必须要实现接口中的所有抽象方法。
  • 抽象类实现接口,必须不实现或只实现部分接口中的抽象方法(留待子类实现)
  • 接口不能被实例化,实现接口的具体类可被实例化
  • 多重继承 :接口可以被继承,并且可以被多继承。
    • 一个类可同时实现一或多个接口
      class Bird implements Flyable, Singable {...}
    • 一个接口可继承一或多个父接口
      interface SmartDevice extends PowerControllable, Networkable {...}
    • 一个接口可被多个子接口继承
  • 用途用法
    需求情况:当多个无继承关系、互不关联的类需要具备相同意图(即相同的方法头)但实现这个相同意图的具体操作(方法体)不同时。
    代码行为:创建一个或多个独立的接口,在其中定义一个/些供实现类实现这个相同意图的方法头(即抽象方法)。实现类实现一个或多个接口时,必须实现接口中全部抽象方法,以具有这个/些相同意图的具体行为(完整方法)
    实现类实现接口的抽象方法的操作
    1. 在类声明后添加 implements 接口名(多个接口用逗号分隔,如 public class Helicopter implements Flyable, Landable {...}
    2. 为每个抽象方法添加 public 修饰符和方法体
  • 意义

抽象类使用案例

一个接口的例子:Flyable 接口,这个接口代表了“能够进行飞”这种能力,任何类只要实现了这个 Flyable 接口的话,这个类也具备了“飞”这种能力,那么就可以用来进行“飞”操作了。

graph LR
A[接口 Flyable] -->|定义| B[fly方法]
C[飞机类 Airplane] -->|实现| A
D[鸟类 Bird] -->|实现| A
E[无人机 Drone] -->|实现| A
public interface Flyable {
    
    // 1. 抽象方法
    void fly();
    // 2. 默认方法
    default void takeOff() {
        System.out.println("起飞!");
    }
    // 3. 静态方法
    static double calculateFuelConsumption(double distance, double efficiency) {
        return distance / efficiency;   //计算燃料消耗量
    }
    // 4. 私有方法
    private void preFlightCheck() {
        System.out.println("执行飞行前检查... 所有系统正常!");
    }
    
    // 另一个默认方法,展示调用静态方法和私有方法
    default void planFlight(double distance) {
        double fuelNeeded = calculateFuelConsumption(distance, 15.5); // 调用静态方法
        System.out.println("飞行计划已制定。需要燃料: " + fuelNeeded + " 升");
        preFlightCheck(); // 调用私有方法
    }
}


// 飞机类实现 Flyable 接口
class Airplane implements Flyable {
    @Override
    public void fly() {
        System.out.println("飞机使用喷气发动机飞行");
    }
    
    // 可以选择重写默认方法
    @Override
    public void takeOff() {
        System.out.println("飞机特定起飞程序:展开襟翼...");
        System.out.println("增加推力... 起飞!");
    }
}

// 鸟类实现 Flyable 接口
class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("鸟儿通过拍打翅膀飞行");
    }
    
    // 使用默认的takeOff方法,不重写
}

// 无人机实现 Flyable 接口
class Drone implements Flyable {
    @Override
    public void fly() {
        System.out.println("无人机使用旋翼飞行");
    }
    
    // 重写默认方法
    @Override
    public void takeOff() {
        System.out.println("无人机垂直起飞:旋转旋翼中...");
        System.out.println("上升中!");
    }
    
    @Override
    public void planFlight(double distance) {
        System.out.println("无人机飞行计划,计算电池使用量");
        // 无人机有不同的效率
        double batteryUsage = Flyable.calculateFuelConsumption(distance, 8.2) / 10; // 调用静态方法
        System.out.println("预计电池使用量: " + batteryUsage + " 千瓦时");
    }
}

// 测试类
class FlightTest {
    public static void main(String[] args) {
        // 使用接口静态方法
        double fuel = Flyable.calculateFuelConsumption(500, 12.5);
        System.out.println("500公里所需燃料: " + fuel + " 升\n");
        
        // 测试不同实现类
        // 声明一个类型为 Flyable 接口的数组,意味着这个数组可以存放任何实现了 Flyable 接口的类的对象
        Flyable[] flyingObjects = {
            new Airplane(),
            new Bird(),
            new Drone()
        };
        
        // 增强型for循环,遍历 flyingObjects 数组中的每个元素(即接口的类的对象,由obj 变量引用)
        for (Flyable obj : flyingObjects) {
            obj.takeOff(); // 调用默认方法(可能被重写)
            obj.fly();     // 调用抽象方法
            obj.planFlight(300); // 调用默认方法
            System.out.println("-----");
        }
    }
}

实现接口中的冲突与解决

常量冲突:当多个父接口定义了同名常量时,子接口/实现类无法自动确定使用哪个常量。

  • 解决:子接口或实现类中必须通过显式指定父接口名来访问(使用父接口.常量名

方法冲突:当多个接口定义了同名同参数的默认方法时,实现类无法自动确定使用哪个接口的默认实现。

  • 解决:同时实现这些接口的类必须重写该冲突方法,并在重写方法中,选择调用特定接口的默认实现(使用接口名.super.方法名()
    interface A { default void foo(){} }
    interface B { default void foo(){} }
    class C implements A, B {
        @Override
        public void foo() { A.super.foo(); } 
    }
    

特殊的:
当方法名相同但参数列表不同时,被视为方法重载(overloading),不会产生冲突,子接口将继承所有重载版本的方法

抽象类与接口的联系与优势

抽象类与接口的联系:

  • 接口:接口与实现类之间形成多对多的关系(接口能多继承:一个接口可以被多个类实现,一个类可以实现多个接口)。
  • 抽象类:抽象类与实现类之间形成一对多的关系(类只能单继承:一个实现类只能继承一个抽象类)

接口相较抽象类的优势:

  • 多重继承:当需要为一个类添加额外的行为规范时,通过实现接口的扩展性较强
  • 不干扰继承关系:通过实现接口,多个类可获得相同能力,不需要修改现有类层次结构(继承关系)
  • 跨继承关系:接口可以跨越不同的继承树,让不同继承树中的类具有相同的行为

N种内部类

类的五大成员:属性、方法、构造器、代码块、内部类

内部类:一种嵌套在其他类中的类,其可以访问外部类的成员变量和方法。包括成员内部类、静态内部类、局部内部类和匿名内部类。

  • 在类的内部定义,与实例变量、实例方法同级别的类
  • 可以无条件地直接访问其外部类的所有成员属性和方法,包括私有成员、静态成员和实例成员(除了,静态内部类不能直接访问实例成员)
  • 可在编译之后生成独立的字节码文件
  • 可为外部类提供必要的内部功能组件

属性重名问题

  • 当外部类、内部类存在重名属性时,内部类调用会优先访问内部类属性值
  • 如果要在内部类中访问外部类的重名属性,可以使用 OuterClass.this. 来显式地指明访问外部类的属性
  • 在局部内部类中,则为外部类与 内部类所在方法 中的属性重名,规则相同
    示例
    public class OuterClass {  
        private int outerInstanceField = 10;  
    
        static class StaticNestedClass {  
            private int outerInstanceField = 20; // 内部类中的重名属性  
    
            public void accessOuterMember() {  
                System.out.println("Inner class field: " + outerInstanceField); 
                // 访问内部类的属性  
                System.out.println("Outer class field: " + OuterClass.this.outerInstanceField);
                // 通过外部类引用来访问外部类的属性
             }  
        }  
    }
    //输出结果
    Inner class field: 20 
    Outer class field: 10
    

静态内部类

使用static关键字修饰的内部类

定义方式:在定义内部类的时候,在类名前加上一个权限修饰符static

  • 可直接创建对象或通过类名访问其中成员,可声明静态成员(相当于一个外部类)
  • 只能直接访问外部类的静态成员
  • 既能声明静态成员也可以声明非静态成员

静态内部类访问外部类实例成员的方法

方法一:使用内部类一个成员变量来持有外部类的实例,并通过该实例来访问外部类的实例成员
public class OuterClass {  
    private int instanceVar = 10;  
  
    public static class StaticNestedClass {  
        private OuterClass outerInstance;  //声明一个名为 outerInstance 的 OuterClass 类型的私有变量
        
        public StaticNestedClass(OuterClass outer) {  //通过构造函数接受一个 OuterClass 对象作为参数
            outerInstance = outer;  // 将传入的外部类对象赋值给 outerInstance 变量
        }  
  
        public void accessInstanceVar() {
            System.out.println("Instance variable: " + outerInstance.instanceVar);   
            //通过outerInstance来访问外部类的实例成员instanceVar
        }  
    }  
}

方法二:定义一个接受外部类实例作为参数的方法,然后通过这个参数来访问外部类的实例成员
public class OuterClass {
    private int outerInstanceField = 10;

    static class StaticInnerClass {
        public void accessOuterMember(OuterClass outer) {  
        //OuterClass outer 表示这个方法接受一个OuterClass类型的对象outer作为参数
            System.out.println(outer.outerInstanceField);  // 通过外部类实例访问其实例成员变量
        }
    }

    public static void main(String[] args) {
        OuterClass outerObject = new OuterClass();
        StaticInnerClass innerObject = new StaticInnerClass();
        innerObject.accessOuterMember(outerObject);
        // ↑调用静态内部类的方法,传入外部类的实例作为参数来访问外部类的实例成员
    }
}

非静态内部类(成员内部类)

定义方式:定义一个内部类,并可以使用四种权限修饰符进行修饰【(public(公有的)>protected(受保护的)> (default)(默认的)> private(私有的)】

  • 成员内部类中不能使用static修饰变量和方法,但是可以定义静态常量 static public

创建成员内部类的对象的语法格式

OuterClass outer = new OuterClass();  
InnerClass inner = outer.new InnerClass(); 
//或者如下
OuterClass.InnerClass inner = new OuterClass().new InnerClass();

局部内部类

  1. 定义的位置
    局部内部类只能定义在方法体、构造器体或静态/实例初始化块内部,无法在类作用域中直接声明。
  2. 无访问修饰符
    类的定义不使用publicprotectedprivatestatic修饰符(但可以用finalabstract)。
  3. 编译后生成独立类文件
    编译后生成名为外部类名$数字局部内部类名.class的文件(如OuterClass$1InnerClass.class )。
  • 局部内部类访问所在方法的局部变量时,该变量必须显式声明为 final 或 赋值后不再进行修改
  • 局部内部类可直接访问变量:符合要求的局部变量、内部类成员、外部类成员,直接通过 成员名 访问
    访问优先级(从内到外):局部变量(方法内的变量) → 内部类成员 → 外部类成员
    有冲突时,可以用 外部类名.this.成员名 优先访问外部类成员
  • 局部内部类的对象,仅能在定义它的代码块内、内部类外 进行实例化和使用

局部变量:指在方法或代码块内部定义的变量。这些变量只在被声明的那个方法或代码块内部有效。不可使用权限修饰符static 进行修饰

示例

public class OuterClass {  
    private int outerVar = 10;  
      
    public void someMethod() {  //------------------------┐
        final int localVar = 20; // 声明为 final           ↓
                                                // 代码块内、内部类外
        class LocalInnerClass {  //---------  ------------↑
            public void printVars() {  
                System.out.println("Local variable: " + localVar); // 访问内部类所在方法中的局部变量
                System.out.println("Outer class variable: " + outerVar); // 访问方法所在的类中的属性
            }  
        }  
          
        LocalInnerClass inner = new LocalInnerClass();  
        inner.printVars(); // 创建局部内部类的对象并调用方法  
    }  
}

匿名内部类

一种没有类名的内部类,通常用于创建只需一次使用的类
定义方式:

  • 没有类名的局部内部类
  • 必须继承一个父类、抽象类或者实现一个接口

语法格式

父类/接口类型 变量名 = new 父类/接口名() {
    类体
};
//变量名是给匿名内部类实例起的名称

类体中的内容

必须有的内容:

  1. 如果匿名内部类实现了一个接口,那么必须实现该接口的所有抽象方法
  2. 如果匿名内部类继承了一个抽象类,那么必须实现该抽象类的所有抽象方法
  3. 如果匿名内部类继承了一个具体类,那么可以不重写任何方法,但通常重写某个方法

可以有的内容(几乎和普通类一样,但有一些限制):

  1. 实例变量
  2. 实例方法
  3. 实例初始化块(即没有static关键字的花括号块)
  4. 静态常量(static final 修饰的变量)
  5. 静态方法(但Java16之前不允许,Java16开始允许静态成员,包括静态方法和静态变量,但通常不常用)
  6. 静态初始化块(同样,Java16之前不允许,Java16开始允许)

静态初始化块和实例初始化块

  • 实例初始化块:在每次创建对象时执行,在构造函数之前执行。用于初始化实例变量或执行一些每创建一个对象都需要执行的代码。
  • 静态初始化块:在类加载时执行,只执行一次。用于初始化静态变量或执行一些只需要执行一次的代码。

在匿名内部类中,实例初始化块是常用的,而静态初始化块在Java16之前是不允许的,Java16开始允许。

示例

// 示例1:实现接口
interface Greeting {
    void greet();
}

public class Test {
    public static void main(String[] args) {
        // 匿名内部类实现接口
        Greeting greeting = new Greeting() {
            private String message = "Hello";  // 可以有自己的属性
            
            @Override
            public void greet() {
                System.out.println(message + " World!");
            }
            
            public void anotherMethod() {  // 可以添加额外方法
                // 但外部只能用Greeting接口声明的方法
            }
        };
        
        greeting.greet();  // 输出:Hello World!
    }
}

// 示例2:继承抽象类
abstract class Animal {
    abstract void sound();
}

public class Test {
    public static void main(String[] args) {
        Animal dog = new Animal() {
            @Override
            void sound() {
                System.out.println("Woof!");
            }
        };
        
        dog.sound();  // 输出:Woof!
    }
}

// 示例3:继承具体类(虽然不常用)
class Base {
    void show() {
        System.out.println("Base");
    }
}

public class Test {
    public static void main(String[] args) {
        Base obj = new Base() {
            @Override
            void show() {
                System.out.println("Anonymous class");
            }
        };
        
        obj.show();  // 输出:Anonymous class
    }
}