一,什么是面向对象?

个人理解:面向对象就好比乐高搭积木,我们通过设计一系列的零件,然后将其拼接成完整的对象。

例如:一辆汽车,我们设计:车轮,发动机,车门,车窗..... 等一系列零件,然后将其拼接成汽车对象,这就是面向对象。

1.1 设计对象并使用

在面向对象中,必须先设计类,才能获取对象;可以这么理解:如果要使用对象,我们要先定义这个对象是什么,它长什么样,它能干什么。重点是这个定义:这个比较抽象,可以理解为我们定义了人类,定义汽车,定义宇宙,定义完毕之后才能变成一个个具体的人,具体的汽车,但是他们都有一个共同的称为:人类,汽车。

其中

  • :是对象共同特征的描述
  • 对象:是真实存在的具体的东西

代码举例

//定义类
public class 类名{
    成员变量(代表属性)
    成员方法(代表行为)
    构造器(代表如何创建这个类)
}
//创建对象
类名 对象名 = new 类名();
//拿到对象后,使用方法
对象名.成员变量;
对象名.成员方法(参数);

在这个代码里面有很多陌生词汇例如:

  • 成员变量:可以理解为,人的年龄,人的性别;
  • 成员方法:可以理解为技能,每个人都会的东西,例如说话,思考。
  • 构造器:代表如何创建这个类

1.2 编码约束

  • 用来描述一类事物的类叫javabean 类

  • java 文件可以定义多个 class 类,但有且只能用一个 public 修饰,且 public 修饰的类必须为代码的文件名

  • 实际开发中一般是一个类一个文件

  • 成员变量完整定义格式:修饰符 数据类型 变量名 =初始值

    如果不给初始值,存在默认值

    数据类型默认值
    byte,short,int,long0
    float,double0.0
    booleanfalse
    引用数据类型null

二,面向对象四大特性

2.1 封装

2.1.1 什么是封装

封装是面向对象的四大特征之一

想象你养了一只猫,你不可能让它满屋子乱跑(否则它会抓坏沙发),而是把它关进一个带操作按钮的智能笼子。这个笼子就是“封装”的体现:

封装的主要作用是:

  • 隐藏细节:你不需要知道笼子的锁是如何工作的
  • 暴露接口:你只能通过笼子外部的按钮喂食、清理
  • 保护对象:猫无法直接破坏你的家具

封装的三大目的:

目的说明示例
数据保护防止外部代码随意修改对象内部状态禁止设置负数的银行余额
实现细节隐藏使用者无需知道内部如何实现,只需调用接口手机充电时不需要知道内部电路如何工作
提高代码可维护性内部逻辑修改时,只要接口不变,外部代码无需改动优化存款计算方式,外部调用代码不受影响

2.1.2 private关键字

在上述封装中有一点:数据保护;即外部代码无法随意修改内部对象的状态,要实现这个就要涉及到 this 关键字了;

什么是 private 关键字

  • 它是权限修饰符
  • 它可以修饰成员(成员变量和成员方法
  • 被private修饰的成员只能在本类中访问

被 private 修饰的成员变量要在对应的类中提供 getter 和 setter 方法来提供赋值取值通道。

通过 private 关键字修饰的变量,方法外部无法直接的调用,访问,这个时候外部代码就无法直接随意的修改我们内部的变量了。我们通过对外暴漏 get 和 set 方法去让外部代码来修改。

2.1.3 this关键字

this指向当前对象自身的引用。想象你正在写一个类的代码,每个对象诞生后,都会自动携带一个隐藏的指针 this,它就像对象的 "身份证",始终指向自己。

作用:区分局部变量和成员变量

  • 成员变量:定义在类中方法的变量
  • 局部变量:定义在方法的变量

就近原则:如果成员变量和局部变量同名,那么调用的时候离哪个变量近调用哪个。

本质:代表方法调用者的地址值

public class Demo{
    int a=10;
    public void method(){
        int a=20;
        System.out.println(a);//就近原则,a=20
        System.out.println(this.a);//this关键字a=10
	}
}

this 关键字一般主要用在 set 方法上例如

public class Cat{
	private int fast;
    public void setFast(int fast){
        // 主要是用来区分成员变量和局部变量的
        this.fast=fast;
    }
}

一般this 常用的就是这么赋值的时候区分变量名相同的俩个变量

2.1.4 构造方法

构造方法也叫构造器,构造函数

作用:在创建对象的时候给成员变量赋值

格式:

public class Student{
    //构造方法
    修饰符 类名(参数){
        方法体
    }
}

注意:

  • 如果没有参数就是空参构造
  • 任何类定义出来,默认带了无参构造,可以不写一旦定义了有参构造,无参构造就没有了,这个时候需要自己手动写
  • 有参数就是有参构造,并且构造方法必须和类名一致,构造方法没有返回值不能用return
  • 创建对象的时候构造方法由jvm虚拟机调用,不需要手动调用
  • 创建一次对象就调用一次构造方法

this() 的用法

在构造方法中通过 this() 调用本类其他构造方法,必须放在第一行

public class Rectangle {
    private int width;
    private int height;
    private String color;

    // 全参数构造方法
    public Rectangle(int w, int h, String c) {
        this.width = w > 0 ? w : 1;
        this.height = h > 0 ? h : 1;
        this.color = c;
    }

    // 调用全参构造设置默认颜色
    public Rectangle(int w, int h) {
        this(w, h, "Black"); // 必须作为第一行
    }

    // 调用双参构造设置默认尺寸
    public Rectangle() {
        this(10, 10); // 再次调用双参构造
    }
}

构建一个标准 JavaBean 类注意要点

  1. 类名要见名知意
  2. 成员变量要用private修饰
  3. 至少提供2种构造方法
    • 无参构造方法
    • 有参构造方法
  4. 成员方法
    • 提供每一个成员变量对应的Get和Set方法
    • 提供类的行为

2.1.5 static关键字

static 定义的变量,方法会存储在堆空间中的静态区,不跟随 new 对象创建在堆内存这个地址中。之后创建多个对象,这些对象中 static 修饰的成员都是引用同一个静态区中的值

static 表示类级别的成员(与对象实例无关),它在类加载时初始化,整个程序生命周期内只存在一份。就像公司里的公共打印机(static 资源),所有人共享使用,而不是每人单独配一台。

static 可以修饰类,方法,变量,被它修饰的类,方法,变量都全局唯一,且是类级别的,叫,静态方法,静态类,静态变量。

  • 静态变量

    • 被该类所有对象共享
    • 不属于对象,属于类
    • 随着类的加载而加载,优先于对象
    • 调用方式:类名.静态变量也可以对象名.静态变量(不推荐)
  • 静态方法

    • 多用在测试类工具类

    • javabean 类中很少使用它

    • 调用方式类名.静态方法名()也可以对象名字.静态方法名()不推荐

      工具类:帮助我们做一些事情,但不描述任何事物的类

      javabean 类:描述一类事务的类,比如 Student,Dog 等

      测试类:用来检查其他类是否书写正确,带 main 方法的类,是程序的入口

  • 静态类

    • 一般不会单独给类修饰为static,常用于内部类,如静态内部类

static 注意事项

  1. 静态方法只能访问静态的内容!
  2. 非静态方法可以访问静态变量或静态方法,也可以访问非静态变量和非静态方法
  3. 静态方法中是没有this关键字的!

静态 vs 非静态核心对比表

特性静态成员非静态成员(实例成员)
内存分配类加载时分配,仅一份对象创建时分配,每个对象独立
访问方式类名.成员对象.成员
生命周期与类共存亡与对象共存亡
可访问范围只能访问静态成员可访问静态和实例成员
this关键字不可用可用
典型应用工具方法、全局配置、单例模式对象特有属性/行为

2.2 继承

2.2.1 什么是继承?

继承(Inheritance) 是面向对象编程中实现代码复用的核心机制。就像孩子会继承父母的特征,子类(派生类)可以自动获得父类(基类)的属性和方法,同时还能发展自己的独特功能。

继承是面向对象四大特征之一,继承就是类之间的父子关系,用extends关键字来让类与类之间建立继承关系。

public Student extends Person{}

Student 成为子类 (派生类),Person 成为父类(基类或超类)

好处:

  • 可以把多个子类中重复的代码抽取到父类中,提高代码复用性
  • 子类可以在父类的继承上,增加其他功能,使子类更加强大

当类与类之间,存在相同(共性)的内容,并满足子类是父类中的一种,就可以考虑使用继承来优化代码

缺点:

  • 代码的耦合性过高,一旦修改了父类,会影响所有的子类,非常不安全

特点

  • java中只支持单继承,不支持多继承,但支持多层继承
  • 每一个类都直接或间接继承于Ojbect类,即Object类是所有类的最终父类

子类能继承父类的内容:

  • 不能继承父类的构造方法
  • 可以继承父类的全部成员变量(包括private修饰的,可以继承不能调用
  • 只能继承父类中非私有的成员方法

继承中成员变量的访问特点:满足就近原则

2.2.2 为什么不允许多重继承

Java 为什么只支持单继承?简单说:为了避免“致命钻石问题”

想象你同时遗传了两个人的特征:

爸爸: 蓝眼睛、会弹吉他
妈妈: 蓝眼睛、会唱歌

你 = 继承(爸爸, 妈妈)  ← 多重继承

问题来了:
1. 你有几双眼睛?基因冲突!
2. 爸爸说:弹吉他要用拨片
   妈妈说:弹吉他要用手指
   你到底听谁的?方法冲突!

经典的“钻石问题”(Diamond Problem)

// 假设Java允许多重继承,会发生什么:
class A {
    void show() {
        System.out.println("A");
    }
}

class B extends A {
    void show() {
        System.out.println("B");
    }
}

class C extends A {
    void show() {
        System.out.println("C");
    }
}

// 如果允许多重继承:D同时继承B和C
class D extends B, C {  // ✗ Java不允许!
    // 问题:D继承了B的show()和C的show()
    // 现在D.show() 应该调用哪个?
    // 是B.show()?还是C.show()?还是A.show()?
}

冲突无法解决! 这就是“致命钻石”

2.2.3 super关键字

super 关键字,跟 this 一样主要是用于区分成员的,调用成员的时候使用父类的成员

调用方法:super.变量名super.方法名()

public class Fu{
	String name="Fu";
}
public class Zi extends Fu{
	String name="Zi";
	public void ziShow(){
		String name="A";
        //先找局部变量,局部变量没有再找子类中的成员变量,子类找不到再去父类找
        //局部--》子类--》父类
		System.out.println(name);//A
        System.out.println(this.name);//Zi
        System.out.println(super.name);//Fu
	}
}

2.2.4 方法重写

方法的重写:当父类的方法不能满足子类现在的需求,需要进行方法重写

  • 书写格式:在继承体系中,子类出现了和父类中一模一样的方法说明,我们就称子类这个方法是重写方法
  • 在重新方法上方加上@Override重新注解

方法重写注意:

  • 重写方法的名字,形参列表必须和父类一致

  • 子类重写方法时,访问权限子类必须大于父类

  • 子类重写方法时,返回值类型子类必须小于父类、

  • 私有方法,静态方法不能重写

    私有方法不能重写是因为子类根本继承不了它

    静态方法不能重写是因为静态的东西全局唯一

2.2.5 继承中构造方法的特点

  1. 父类中构造方法不会被子类继承

  2. 子类中所有的构造方法默认先访问父类中的无参构造再执行自己

    因为子类在初始化的时候,可能会使用到父类中的数据,如果父类没有完成初始化,子类就无法使用父类的数据了。

    所以子类初始化之前,一定要先调用父类构造方法先完成父类数据空间的初始化。

调用父类的无参构造:super()这个语句会默认出现在子类无参构造的第一行,如果你只是想要父类的默认初始化就别写这玩意了。

如果想自己给父类的成员变量赋值就在子类的构造方法中第一行手写super进行调用赋值

举例写法:

public class Fu{
    String name;
    int age;
    public Fu(){
        System.out.println("父类的无参构造");
    }
    public Fu(String name,int age){
        this.name=name;
        this.age=age;
    }
}

public class Zi extends Fu{
    public Zi(){
        super();
        System.out.println("子类中的无参构造");
    }
    public Zi(String name,int age){
        super(name,age);//这样写就可以指定参数给父类初始化了
    }
}

2.2.6 this和super关键字对比

  • this 代表一个变量,表示当前方法调用者的地址值
  • super 代表父类的存储空间
关键字访问成员变量访问成员方法访问构造方法
thisthis.成员方法 访问的是本类成员变量this.成员方法(参数列表) 访问的是本类成员方法this(参数列表) 访问的是本类构造方法
supersuper.成员方法 访问的是父类成员变量super.成员方法(参数列表) 问的是父类成员方法super(参数列表) 问的是父类构造方法

2.3 多态

多态是面向对象的四大特性之一

  • 多态的定义是:同种类型的对象表现出不同形态。
  • 多态的表现形式:父类类型 对象名=new 子类对象()

多态是设计模式最重要的,可以相当于设计模式的基石,可以这么说,没有多态就没有设计模式。

2.3.1 多态的前提

使用多态需要有条件

  1. 有父类引用指向子类对象Fu f=new Zi();
  2. 有继承和实现关系
  3. 有方法的重写

使用多态可以在方法中使用父类类型作为参数,接收所有的子类对象,并且根据传递的子类对象不同使用子类重写的方法

2.3.2 多态调用成员的特点

  • 变量调用:编译看左边,运行也看左边
  • 方法调用:编译看左边,运行看右边

当子类和父类中出现同名的成员变量的时候调用规则:

  • 编译看左边:javac 编译代码的时候,会看左边的父类中有没有这个变量,如果没有就编译失败,如果有就编译成功。

  • 运行也看左边:java 运行代码的时候,实际获取的是左边父类成员变量的值

当子类和父类中出现同名方法调用规则:

  • 编译看左边:javac 编译代码的时候,会看左边父类中有没有这个成员方法,如果没有就编译失败,如果有就编译成功

  • 运行看右边:java 运行代码的时候,实际上运行的是子类中的方法

理解

  • Animal a=new Dog()Dog d=new Dog()

    子类继承了父类的这个 name 成员变量,导致存在俩个 name

    • a 因为是 Animal 父类类型的所以调用的是父类的 name
    • d 因为是 Dog 子类类型的所以调用的是子类中的 name
  • 成员方法:如果子类对方法进行了重写,那么在虚方法表中是会把父类的方法覆盖了的所以调用的是子类重写的方法

2.3.3 多态的优缺点

优点:

  • 在多态形势下,右边对象可以实现解耦合,便于扩展和维护
  • 定义方法的时候,使用父类类型作为参数,可以接收所有子类对象,体现多态的扩展性与便利性

缺点:

  • 不能调用子类特有的功能(子类新写出来的父类中没有的方法

    因为在编译的时候会先检查左边父类有没有这个方法,父类没有故报错

    解决方法:变回子类类型(强制类型转换!)

    但是,在强转的时候与真实类型不一致会导致报错

    解决方法instanceof关键字:判断变量的类型是否为指定类型

    a instanceof b如果 a 是 b 的类型就返回 true

jkd14 新特性

//a如果是Dog类型就强转为Dog类型并且转换后变量名为d,如果不是则不转换
if(a instanceof Dog d){
    d.lookhome();
}else{
    System.out.println("没有这个类型无法转换");
}

2.4 抽象

抽象是面向对象四大特性之一:抽象是设计思想,从具体中提取共性,定义规范。

2.4.1 抽象类和抽象方法

想象你要造一辆车:

  • 工程师会先画设计图纸,定义必须有的部件(如发动机、方向盘)
  • 但图纸不具体实现这些部件(比如不指定发动机是V6还是电动)
  • 不同厂商根据这张图纸生产具体车型(燃油车、电动车)

映射到代码

  • 抽象类 = 设计图纸(定义必须有的东西,但不具体实现)
  • 抽象方法 = 图纸上的强制要求(如"必须要有发动机")
  • 具体子类 = 实际生产的车型(实现具体要求)

抽象类

  • 只有方法声明,没有方法体(没有大括号里的具体实现)

  • abstract 关键字标记

  • 强制要求子类必须实现该方法

  • 示例:

    // 抽象方法示例
    abstract void startEngine(); // 没有大括号,直接分号结束
    

抽象方法

  • abstract 修饰的类

  • 可以包含抽象方法(普通类不能有抽象方法)

  • 不能被实例化(不能直接 new AbstractClass()

  • 主要作用是为子类定义规范

  • 示例

    abstract class Vehicle { // 抽象类
        // 抽象方法(没有实现)
        abstract void startEngine();
    
        // 普通方法(有实现)
        void turnOnHeadlight() {
            System.out.println("打开大灯");
        }
    }
    

注意事项

  • 抽象类不能实例化
  • 抽象类中不一定有抽象方法(抽象类里面也可以有普通方法),但是有抽象方法的类一定是抽象类
  • 抽象类可以有构造方法
  • 抽象类的子类:要么重写抽象类中全部的抽象方法,要么子类也是一个抽象类

抽象类和抽象方法的意义:强制子类必须按照规定的格式书写!方便调用

使用场景

  1. 需要定义通用行为模板,但某些行为必须由子类自定义
  2. 多个类有共同特征,但部分功能实现方式不同
  3. 需要限制直接实例化(比如"动物"不应该被直接创建,但"猫"、"狗"可以)

典型案例

  • java.io.InputStream(Java IO库中的抽象类)
  • GUI编程中的 javax.swing.JComponent
  • 游戏开发中的 GameCharacter 基类

2.4.2 接口

接口是一种规则,是对行为的抽象。想让哪个类有一个行为就让这个类实现对应的接口即可

抽象类更多是用在父类中,接口更多的用于行为。

接口的多态:当一个方法的参数是接口时,可以传递接口中所有的实现类的对象!

定义接口

public interface 接口名{
    //方法
}

接口的特点:

  1. 接口不能实例化
  2. 接口和类之间是实现关系,通过implements关键字表示:public class 类名 implements 接口名{}
  3. 接口的子类(实现类):要么重写接口中所有抽象方法,要么是抽象类
  4. 接口和类的实现关系,可以是单实现,可以是多实现:public class 类名 implements 接口名1,接口名2{}
  5. 接口还可以在继承一个类的同时实现多个接口:public class 类名 extends 父类 implements 接口1,接口2{}

接口中成员的特点:

  1. 接口中的变量只能是常量,默认修饰符public static final
  2. 接口中没有构造方法
  3. jkd7之前接口中的成员方法只能为抽象方法,默认修饰符public abstract

接口和类之间的关系

  • 类和类之间

    继承关系,只能单继承,不能多继承,但是可以多层继承

  • 类和接口关系

    实现关系,可以单实现,也可以多实现,还可与在继承一个类的同时实现多个接口

  • 接口和接口间的关系

    继承关系,可以单继承,也可以多继承

    如果此时实现类是实现了最下面的子接口,那么要重写接口继承体系全部的抽象方法

JDK8 之后接口中新增的方法

  • 默认方法

    允许接口中定义默认方法,需要使用关键字default修饰

    作用:解决接口升级问题

    • 当接口新增了多个方法的时候,** 实现类需要实现接口中新增的方法。** 此时在接口中写了默认方法,可以不用在每个实现类中实现对应的方法当我们要用到某个方法的时候,在实现类中重写即可想在调用接口中默认方法直接在实现类中调用即可

    • 接口中默认方法的定义格式

      public default 返回值类型 方法名(参数列){
          
      }
      
    • 注意事项:

      默认方法不是抽象方法,不强制被重写,但是如果被重写,重写的时候去掉 default 关键字

      public 可以不写,default 必须写

      如果实现了多个接口,多个接口中存在名字相同的默认方法子类就必须对该方法重写

  • 静态方法

    • 接口中定义静态方法,需要用static关键字修饰

    • 接口中静态方法的定义格式

      public static 返回值类型 方法名(参数列){}
      
    • 静态方法只能用接口名调用,静态方法不能被重写。

  • 私有方法

    • 定义格式:

      //私有方法 
      private 返回值类型 方法名(参数列){方法体}
      //静态的私有方法
      private static 返回值类型 方法名(参数列){方法体}
      
    • 私有方法是提供给接口中默认方法抽取重复部分代码使用的

    • 静态私有方法是提供给接口中静态方法抽取重复部分代码使用的

    • JDK9 以后才有的私有方法

三,Java语言特性

3.1 包

包就是文件夹,用来管理不同功能的 java 类,方便后期代码维护

import关键字,导入其他包用来使用其他包的类

  • 使用同一个包中的其他类,不需要导入包
  • 使用java.lang包中的类,不需要导包
  • 如果同时使用俩个包,并且来个包有同名类,需要使用全类名包名.类名

3.2 final关键字

用 final 修饰的方法:代表该方法是最终方法,不能被重写

用 final 修饰的:代表该类是最终类,不能被继承

用 final 修饰的变量:叫常量,只能被赋值一次

常量:实际开发中,常量一般作为系统的配置信息,方便维护,提高可读性

常量命名规则:

  • 单个单词:全部大写
  • 多个单词:全部大写,单词见用下划线隔开

细节:

  • final修饰的变量是基本数据类型的:那么变量存储的数据值不能改变
  • final修饰的变量是引用数据类型的:那么变量存储的地址值不能改变,对象内部可以改变
  • 常量一般的写法private static final 常量名=值

3.3 权限修饰符

  • 权限修饰符:用来控制一个成员能够被访问的范围
  • 可以修饰成员变量,方法,构造方法,内部类
修饰符同一个类中同一个包中不同包下的子类不同包
private
空着不写
protected
public

3.4 代码块

代码块:用大括号括起来的部分

3.4.1 局部代码块

局部代码块:写在方法里面的一对单独的大括号

作用:因为变量作用范围是在所属大括号内,这玩意用来节约内存空间,现在基本上用不到了!

public class Main {
    public static void main(String[] args) {
        {
            int a=10;
        }
        System.out.println(a);//此时会报错,因为是局部代码块,变量a作用范围只在大括号内
    }
}

3.4.2 构造代码块

作用:写在成员位置中,会优先于构造方法执行。一般用来把多个构造方法中重复的部分写在代码块中。

import lombok.*;

public class Student {
    
    @Getter@Setter
    private int age;
    
    @Getter@Setter
    private String name;
    
    {
        System.out.println("多个构造方法中重复的部分");
    }
    
    public Student(){
        
    }
    public Student(String name,int age){
        this.age=age;
        this.name=name;
    }
    
}

13.3 静态代码块

static修饰的代码块,随着类的加载而加载,并且自动触发,只执行一次

在类加载的时候,给类中数据初始化的时候使用

格式:static{}

随着类的加载而加载,只执行一次

创建多个同一个类的对象的时候,静态代码块只会第一次创建对象的时候执行,并且只执行一次

3.5 内部类

3.5.1 什么是内部类

内部类是类的五大成员之一

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

在一个类里面定义的类,这个类就叫内部类

定义内部类的时候遵守

  • 内部类表示的事物是外部类的一部分
  • 内部类单独出现没有任何意义
  • 比如说车和引擎,车就是外部类,引擎就是内部类

内部类访问特点:

  • 内部类可以直接访问外部类的成员,包括私有的
  • 外部类要访问内部类的成员,必须创建对象

内部类的作用:

  1. 逻辑分组:如果类A只被类B使用,可以将A放在B内部,避免污染全局命名空间。
  2. 访问外部类成员:内部类可以直接访问外部类的所有成员(包括私有成员)。
  3. 实现多重继承:通过内部类间接实现类似多重继承的效果(Java不支持多继承,但可以内部类模拟)。
  4. 隐藏实现细节:将内部类设为私有,对外完全隐藏。

3.5.2 成员内部类

定义:直接定义在外部类的成员位置(类似类的成员变量)。

特点

  • 必须依附外部类对象存在(不能独立存在)。
  • 可以访问外部类的所有成员(包括私有)。
  • 成员内部类可以被一些修饰符修饰:private,默认,protected,public等
public class Outer {
    private int outerField = 10;

    // 成员内部类
    class Inner {
        void print() {
            System.out.println("访问外部类的私有字段: " + outerField); // 直接访问外部类私有成员
        }
    }

    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner(); // 必须通过外部类实例创建内部类
        inner.print(); // 输出: 访问外部类的私有字段: 10
    }
}

3.5.3 静态内部类

定义:用 static 修饰的内部类。

特点

  • 不依赖外部类实例(可直接创建)。
  • 不能直接访问外部类的非静态成员(只能访问静态成员)。
public class Outer {
    private static int staticOuterField = 20;
    private int instanceOuterField = 30;

    static class StaticInner {
        void print() {
            System.out.println("访问外部类的静态字段: " + staticOuterField);
            // System.out.println(instanceOuterField); // 错误!不能访问非静态成员
        }
    }

    public static void main(String[] args) {
        Outer.StaticInner inner = new Outer.StaticInner(); // 直接创建,无需外部类实例
        inner.print(); // 输出: 访问外部类的静态字段: 20
    }
}

3.5.4 局部内部类

定义:定义在方法或代码块内部的类。

特点

  • 仅在方法或代码块内可见。
  • 可以访问外部类的成员,但若访问方法的局部变量,该变量必须声明为 final 或等效 final(Java 8+)
public class Outer {
    private int outerField = 40;

    public void someMethod() {
        final int localVar = 50; // 局部变量必须final或等效final

        class LocalInner {
            void print() {
                System.out.println("外部类字段: " + outerField);
                System.out.println("局部变量: " + localVar); 
            }
        }

        LocalInner inner = new LocalInner();
        inner.print(); // 输出: 外部类字段: 40  局部变量: 50
    }

    public static void main(String[] args) {
        new Outer().someMethod();
    }
    
}

3.5.5 匿名内部类

定义:没有名字的局部内部类,通常用于快速实现接口或抽象类。

  • 匿名内部类的本质就是隐藏了名字的内部类

特点

  • 简洁但可读性差(适合一次性使用)。
  • 常见于GUI事件处理(如按钮点击)或Java 8之前的Lambda替代。

匿名内部类创建格式:

new 类名或接口名(){
    
    重写方法;//这里重写的是new对应的类or接口的方法
    
};//这里有分号!

//真正的类是
{
    重写方法;
}

//new 类名或接口名()是创建这个类的对象

举例

public interface Greeting {
    void greet();
}

public class Demo {
    public static void main(String[] args) {
        // 匿名内部类:直接实现接口
        Greeting greeting = new Greeting() {
            @Override
            public void greet() {
                System.out.println("你好,匿名内部类!");
            }
        };
        greeting.greet(); // 输出: 你好,匿名内部类!
    }
}
  • 如果 new 的是接口那么是实现关系
  • 如果 new 的是类那么是继承关系

应用场景:

public class Main {
    public static void main(String[] args) {
        //如果我想调用method方法,那么需要创建一个dog子类继承Animal类
        //通过dog类来传递数据给method
        //但是如果我只用dog类一次,还需要单独定义一个类就很麻烦!
        //现在可以通过内部类直接写一个匿名实现类传递数据给mthod
        
        method(
            new Animal(){}//将这个当作Animal的子类对象!
        );
    }
    
    public static void method(Animal a){
        
    }
    
}
//当方法的参数是接口or类的时候
//以接口为例:可以传递这个接口的实现类对象
//如果实现类只用一次,就用匿名内部类简化代码

3.5.6 内部类的底层原理

成员内部类匿名内部类在编译后会生成独立的 .class 文件,命名格式为:

  • 成员内部类:外部类$内部类.class(如 Outer$Inner.class)。
  • 匿名内部类:外部类$1.class(按顺序编号)。

内部类访问外部类成员时,实际上是通过持有外部类对象的引用(编译器自动处理)。

用内部类实现迭代器

public class MyList {
    private int[] data = {1, 2, 3, 4, 5};

    // 通过内部类实现迭代器(隐藏实现细节)
    public Iterator getIterator() {
        return new ListIterator();
    }

    // 成员内部类实现Iterator接口
    private class ListIterator implements Iterator {
        private int index = 0;

        @Override
        public boolean hasNext() {
            return index < data.length;
        }

        @Override
        public Integer next() {
            return data[index++];
        }
        
    }

    public static void main(String[] args) {
        MyList list = new MyList();
        Iterator it = list.getIterator();
        while (it.hasNext()) {
            System.out.print(it.next() + " "); // 输出: 1 2 3 4 5
        }
    }
    
}

3.6 Lambda表达式

Lambda 表达式最基础的特点就是优化匿名内部类的书写,在了解 Lambda 表达式之前需要知道函数式编程

函数式编程:忽略面向对象复杂的语法,强调做什么,而不是谁去做,即更加关注方法体中的逻辑,而不是对象。

  1. Lambda 表达式是 JDK8 开始后的一种新语法

    书写格式:

    ()->{
        
    }
    
    • ()对应方法的形参
    • ->固定格式
    • {}对应着方法的方法体
  2. 举例:

    //匿名内部类写法
    Arrays.sort(arr,new Comparator<Integer>(){
       @Override
        public int compare(Integer o1,Integer o2){
            return o1-o2;
        }
    });
    
    //Lambda简化后写法
    Arrays.sort(arr,(Integer o1,Integer o2)->{
        return o1-o2;
    });
    

小细节:

  1. Lambda 表达式可以用来简化匿名内部类的书写

  2. Lambda 表达式只能简化函数式接口的匿名内部类的写法

    函数式接口有且仅有一个抽象方法的接口叫函数式接口,接口上方可以加上@FunctionalInterface注解

    加上注解之后如果不满足函数式接口条件会报错

Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码,它可以写出更简洁,更灵活的代码。

作为一种更紧凑的代码风格,使 Java 语言表达能力得到提升。

Lambda 表达式省略写法

写法规则:

  1. 参数类型可以不写
  2. 只有一个参数,小括号可以不写
  3. 如果Lambda表达式的方法体只有一行,大括号,分号,return可以不写,需要同时不写!

省略核心:可推导,可省略

public class Main {
    public static void main(String[] args){
        Integer[] arr=new Integer[]{5,1,2,7,8,3,4};
        Arrays.sort(arr, (o1,o2)->o2-o1);//究极简化写法
    }
}

了解即可,idea 可以自动将匿名内部类转化为 lambda 表达式的。

3.7 枚举类

3.7.1 什么是枚举类?

枚举(Enum)是 Java 中一种特殊的类,用于定义一组固定的常量。它比传统的常量定义(如 public static final)更安全、更灵活。

为什么需要枚举类?

假设你需要表示“星期几”,用传统方式可能会这样写:

public class Day {
    public static final int MONDAY = 1;
    public static final int TUESDAY = 2;
    // ...其他天
}

问题:

  1. 类型不安全int 类型可能被传入非法值(比如 Day.MONDAY + 100)。
  2. 可读性差:打印时只能看到数字,无法明确含义。
  3. 无法扩展:无法为每个常量添加额外属性或方法。

枚举的解决方案

public enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
  • 每个枚举常量都是 public static final实例。
  • 枚举的构造方法默认是 private,无法通过 new 创建对象。
  • 枚举在类加载时初始化,保证线程安全。

3.7.2 枚举的核心特性

  1. 枚举是类

    枚举本质是一个类(继承自 java.lang.Enum),编译后会生成 .class 文件。每个枚举常量都是该类的一个实例(单例)。

    // 编译后:Day.class
    public enum Day {
        // 等同于调用构造函数:public static final Day MONDAY = new Day();
        MONDAY, 
        TUESDAY,
        // ...
    }
    
  2. 可以添加属性和方法

    枚举可以像普通类一样定义字段、方法和构造函数。

    public enum Planet {
        // 枚举常量 + 构造参数
        MERCURY(3.303e+23, 2.4397e6),
        VENUS(4.869e+24, 6.0518e6);
    
        private final double mass;   // 质量(kg)
        private final double radius; // 半径(m)
    
        // 构造方法(必须私有)
        private Planet(double mass, double radius) {
            this.mass = mass;
            this.radius = radius;
        }
    
        public double getMass() { return mass; }
        public double getRadius() { return radius; }
    }
    
  3. 可以实现接口

    枚举可以实现接口,为每个常量提供不同的行为。

    public interface Operation {
        double apply(double x, double y);
    }
    
    public enum BasicOperation implements Operation {
        PLUS {
            public double apply(double x, double y) { return x + y; }
        },
        
        MINUS {
            public double apply(double x, double y) { return x - y; }
        };
    }
    
  4. 可以在 switch 中使用

    枚举与 switch 语句天然契合。

    Day day = Day.MONDAY;
    switch (day) {
        case MONDAY -> System.out.println("工作日开始");
        case SATURDAY, SUNDAY -> System.out.println("休息日");
    }
    

3.7.3 枚举的常用方法

枚举继承自 Enum 类,自带以下方法:

方法作用
values()返回所有枚举常量(如 Day.values()
valueOf(String)根据名称返回枚举常量(如 Day.valueOf("MONDAY")
name()返回枚举常量名称(如 MONDAY.name() → "MONDAY")
ordinal()返回枚举常量的序号(从0开始)

3.7.4 枚举的实际应用场景

  1. . 状态机(如订单状态)

    public enum OrderStatus {
        CREATED {
            @Override
            public void next(Order order) {
                order.setStatus(PAID);
            }
        },
        PAID {
            @Override
            public void next(Order order) {
                order.setStatus(SHIPPED);
            }
        },
        SHIPPED {
            @Override
            public void next(Order order) {
                order.setStatus(DELIVERED);
            }
        };
    
        public abstract void next(Order order); // 抽象方法,每个状态必须实现
    }
    
  2. 单例模式(线程安全)

     public enum Singleton {
         INSTANCE;  // 单例实例
     
         public void doSomething() {
             System.out.println("单例方法");
         }
     }
     // 使用:Singleton.INSTANCE.doSomething();
    
  3. 错误码

    public enum ErrorCode {
         SUCCESS(0, "成功"),
         PARAM_ERROR(1001, "参数错误"),
         SYSTEM_ERROR(5001, "系统错误");
     
         private final int code;
         private final String message;
     
         ErrorCode(int code, String message) {
             this.code = code;
             this.message = message;
         }
     
         // Getter方法
     }
    

3.7.5 枚举 vs 常量类

对比维度枚举常量类(public static final)
类型安全是(强类型)否(通常是int/String)
可扩展性可添加方法、属性无法扩展
序列化自动支持,安全需要自行处理
switch支持天然支持仅支持基本类型
单例模式天然线程安全需额外处理(如双重检查锁)

注意事项

  1. 不要滥用枚举:枚举比常量类占用更多内存(每个常量都是对象)。
  2. 避免在枚举中定义可变字段:枚举常量本质是单例,应保持不可变。
  3. 优先使用枚举代替常量:在需要类型安全、可扩展性时选择枚举。

3.8 泛型

3.8.1 什么是泛型

泛型:是 JDK5 中引用的特性,可以在编译阶段约束操作的数据类型,并检查。

泛型的格式:<数据类型>

注:泛型只能支持引用数据类型

  • JDK5 引用泛型的原因。

    如果没有给集合指定类型,默认认为所有的数据类型都是 Object 类型。这样在获取数据的时候,无法使用它特有的行为。

  • 泛型的好处

    统一了数据的类型,把运行期间的问题提前到了编译期间,避免了强制类型转换可能出现的异常,因为在编译阶段类型就能确定。

扩展:Java 中的泛型是伪泛型

解释:

Java 的泛型只是在编译阶段检查集合的数据类型,在数据存入集合后,数据在集合中还是 Object 类型

当取出数据的时候,Java 会在底层将取出的数据的数据类型强制转化为泛型限定的那个数据类型。

在编写.java 文件的时候,泛型是存在的,但是当,java 文件编译为.class 文件的时候,泛型会消失。这个过程叫泛型的擦除。

2953321-20251202221500433-1019289391.png

泛型细节

  1. 泛型中不能写基本数据类型

    因为数据在集合中是转化为 Object 类型存储的,基本数据类型不能转化为 Object 类型

  2. 指定泛型的具体类型后,传递数据时,可以传入该类类型或其子类类型。

  3. 如果不写泛型,类型默认是 Object

泛型可以写在:

  • 类后面-->泛型类
  • 方法上面-->泛型方法
  • 接口后面-->泛型接口

3.8.2 泛型类

使用场景:当一个类中,某个变量的数据类型无法确定时,就可以定义带有泛型的类。

修饰符 class 类名<类型1,类型2,类型3...>{
    
}
public class ArrayList<E>{
    //这里的E表示不确定的数据类型,只有创建该类对象时我们手动指定这个E的类型
    //这里的E可以理解为变量,但不是用来记录数据的,而是记录数据类型的,可以写T,E,K,V等表示
    //当我们定义了泛型类后,这个E就代表一种数据类型了!!可以和其他数据类型一样使用。
    Object[] obj=new Object[10];
    int size;
    public boolean add(E e){
        obj[size]=e;
        size++;
        return true;
    }
    public E get(int index){
        return (E)obj[index];
    }

    @Override
    public String toString() {
        return Arrays.toString(obj);
    }
}
//我们在创建这个类的对象的时候需要手动指定这个E的类型!
ArrayList<String> list=new ArrayList<>();//这里手动指定E为String类型了!

3.8.3 泛型接口

使用场景:当一个接口中数据类型不确定的时候使用

//定义单个泛型
修饰符 interface 接口名<类型>{
    
}
//定义多个泛型
修饰符 interface 接口名<类型1,类型2,类型3...>{
    
}

使用方式

//泛型接口
public interface text<E>{
   
}
//1.实现类给出具体的类型
public arry implements text<String>{
    
}
//2.实现类延续泛型接口的泛型,当创建实现类对象的时候再确定泛型类型
public arry<E> implements text<E>{
    
}

3.8.4 泛型方法

使用场景:当方法中形参类型不确定的时候,可以使用泛型

格式:

修饰符 <类型1,类型2,类型3...> 返回值类型 方法名(类型 变量名){    
}

2953321-20240509103405321-789194069.png

举例:

public class ArrayList<E>{
    public boolean add(E e){//类名后面定义的泛型
        obj[size]=e;
        size++;
        return true;
    }
}
public class ArrayList{
    public <E> boolean add(E e){//在方法上面声明的泛型,这个泛型只有本方法可以使用!
        obj[size]=e;
        size++;
        return true;
    }
}

如果只是单一的一个方法不知道形参类型,建议使用泛型方法。

泛型方法的类型是当方法被调用的时候确定的!

3.8.5 泛型的继承和通配符

泛型不具备继承性,但数据具备继承性。

import java.util.ArrayList;

public class Main {
    public static void main(String[] args){
        ArrayList<Ye> list1=new ArrayList<>();
        ArrayList<Fu> list2=new ArrayList<>();
        ArrayList<Zi> list3=new ArrayList<>();
        //泛型不具备继承性
        method(list1);
        //method(list2);报错,无法传入
        //method(list3);报错,无法传入
		
        //数据具备继承性
        list1.add(new Ye());
        list1.add(new Fu());
        list1.add(new Zi());
    }
    public static void method(ArrayList<Ye> list){
    }
}
class Ye{}
class Fu extends Ye{}
class Zi extends Fu{}

虽然上述 method 方法可以使用泛型方法来解决但是,利用泛型方法会存在:该方法可以接收任意的数据类型。

一般的希望:本方法虽然不确定类型,但以后希望只能传递 Ye Fu Zi 这种具备继承结构的类型

解决方法:使用泛型的通配符

通配符?表示不确定的类型,可以进行类型的限定!

? extends E//表示可以传递E或者E的所有子类类型,上限技术
? super E//表示可以传递E或者E的所有父类类型   下限技术
? //表示可以接收所有类型

故上面问题可写成

public class Main {
    public static void main(String[] args){
        ArrayList<Ye> list1=new ArrayList<>();
        ArrayList<Fu> list2=new ArrayList<>();
        ArrayList<Zi> list3=new ArrayList<>();
        method(list1);
        method(list2);
        method(list3);
    }
    public static void method1(ArrayList<? extends Ye> list){//这个表示只能接收Ye类和Ye类的子类
    }
    public static void method2(ArrayList<? super Zi> list){//这个表示只能接收Zi类和Zi类的父类
    }
    public static void method2(ArrayList<?> list){//这个表示可以接收所有数据类型
    }
}
class Ye{}
class Fu extends Ye{}
class Zi extends Fu{}

应用场景:

  1. 如果我们在定义类,方法,接口的时候,类型无法确定,就可以定义泛型类,泛型方法,泛型接口

  2. 如果类型不确定,但是能找到只能传递某个继承体系中的数据,就可以使用泛型的通配符了!

    通配符关键的:可以限定类的范围

  3. 泛型的通配符一般是用在方法中

  4. 通配符也可用做集合中

    ArrayList<? super Number> list=new ArrayList<>();
    

PECS 原则:Producer Extends, Consumer Super

  • 生产者(Producer)用 <? extends T> → 只读,要取出来用
  • 消费者(Consumer)用 <? super T> → 只写,要放进去用

场景对比表

场景应该用为什么示例
需要方法返回值<? extends T>我要从集合里元素用,保证取出的都是T或子类T getFirst(List<? extends T> list)
需要方法参数<? super T>我要往集合里元素,保证能放入T或父类void add(List<? super T> list, T item)
既读又写不要用通配符既要取又要放,直接用具体类型<T> void copy(List<T> dest, List<T> src)

3.8.6 泛型类型参数命名规范:T、E、K、V到底什么时候用?

速记:

  • T:Type(类型) - 通用的单个类型
  • E:Element(元素) - 集合中的元素类型
  • K:Key(键) - Map中的键类型
  • V:Value(值) - Map中的值类型
  • 其他:R(Result)、U(Second Type)...