JAVA面向对象OOP与异常

对象 类 抽象类 封装 继承 多态 接口 异常 错误

java面向对象 OOP与异常

什么是面向对象

  • 面向过程思想

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

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

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

面向对象编程 Object-Oriented Programming OOP

本质:以类的方式组织代码,以对象的组织(封装)数据

抽象

三大特性

  • 封装
  • 继承
  • 多态

从认识论角度考虑是先有对象后有类。对象,是具体的事物。类,是抽象的,是对对象的抽象

从代码运行角度考虑是先有类后有对象。类是对象的模板

回顾方法的定义

方法的定义

1
2
3
4
5
6
修饰符
返回类型
break 跳出swith 结束循环和return 结束方法的区别
方法名 见名知意 注意规范
参数列表 (参数类型 参数名...)
异常抛出 throws IOException

方法的调用

1
2
3
4
5
6
7
8
9
10
静态方法
非静态方法
形参与实参
值传递和引用传递

this关键字 当前这个类或者当前这个对象
this本质:指向当前实例对象的引用变量(存的是对象地址)
作用:
- 区分成员变量和局部变量(同名冲突)
- 在构造方法中调用本类其他构造方法 this(参数)必须写在方法的第一行

类与对象的创建

类与对象的关系

  • 类是一种抽象的数据类型,它是对某一类事物整体描述/定义,但是并不能代表某一具体的事物

    • 动物、植物、手机、电脑…
    • Person类、Pet类、Car类等,这些类都是用来描述/定义某一类具体的事物应该具备的特点和行为
  • 对象是抽象概念的具体实例

    • 张三就是人的一个具体实列,张三家里的旺财就是狗的一个具体实例
    • 能够体现出特点,展现出功能的是具体的实例,而不是一个抽象的概念

创建与初始化对象

  • 使用new关键字创建对象
  • 使用new关键字创建的时候,除了分配内存空间之外,还会给创建好的对象进行默认的初始化以及对类中构造器的调用
  • 类中的构造器也称为构造方法,是在进行创建对象的时候必须要调用的。并且构造器有以下俩个特点:
    • 必须和类的名字相同
    • 必须没有返回类型,也不能写void

构造器详解 必须掌握

一个类即使什么都没有,也存在构造器

new对象的时候,自动调用的特殊方法,作用就是【对象创建出来的时候,给对象做初始化】

构造器不是用来造对象的!new才是在堆开辟内存、创建对象本体;构造器是对象内存分配好之后,马上执行,给对象的成员变量初始化赋值

构造器长啥样

1
2
3
名字必须和类型一摸一样,大小写都要相同
没有返回值,连void都不能写
只能在new的时候自动调用,不能手动直接用方法名去调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Man{
//如果你自己这一行都不写构造器,编译器会自动帮你生成一个无参默认构造器
public Man(){

}
}

class Man{
// 自己写了有参构造
public Man(int age){
}
}
// 此时 new Man(); 直接报错!默认无参构造被干掉了。
// 想要无参,就得自己手动写出来

Man m = new Man(18);
/*
执行顺序拆解
1.new Man(18) → JVM 在堆开辟一块内存,创建 Man 对象,成员变量先给默认值(age=0)
2.调用 Man 构造器,把 18 传进去
3.构造器内部代码执行:给成员变量重新赋值 this.age = age,age 从 0 改成 18
4.构造器执行完毕,返回堆对象地址,赋值给栈上引用变量m
关键点:堆内存先开辟,对象先有默认值,再跑构造器做初始化
*/

两种构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//无参构造
class Man{
int age;
public Man(){
age = 18; // new的时候自动设置age=18
}
}
Man m = new Man(); //自动调用无参构造


//有参构造
class Man{
int age;
String name;
//有参构造
public Man(int age,String name){
this.age = age;
this.name = name;
}
}
// new对象的同时直接给属性赋值,一步到位
Man m = new Man(20,"小明");

构造器重载+this() 在构造器之间的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//构造器也可以重载 同一个类,多个构造器,参数列表不一样
//this(...) 专门用来在一个构造器里面调用本类另一个构造器 硬性规则:必须写在第一行!
class Man{
int age;
String name;

//无参构造
public Man(){
this(18,"无名"); //调用下面的有参构造,必须第一行
}
//有参构造
public Man(int age,String name){
this.age = age;
this.name = name;
}
}
Man m1 = new Man(); //走无参 → 内部调用this(18,"无名")
Man m2 = new Man(20,"小红"); //直接走双参构造

解释说明:

  • 语法上,构造器长得像方法,但它不是普通的方法,它的使命是对象诞生瞬间的初始化,这件事情普通方法做不到,所以单独搞出构造器

    如果程序员忘记调用init() ,对象就带着默认脏数据直接使用,产生bug
    new和构造器绑定在一起,new的同时必然执行初始化,无法跳过,这就是设计它最核心的理由

  • 第二个用处:有参构造,new的时候直接给属性赋值,一行代码,创建对象+给属性赋值一步到位

  • 第三个用处:构造器支持重载,this()构造器互调

    可以规定多种对象创建方式,无参、单参、多参,控制对象创建的规则

    构造器只是语法上长得像方法,但是它不属于对象的行为(比如 miao () 叫、run () 跑,是对象能干的动作)。

    构造器不是对象的 “行为”,它是对象创建的初始化流程。

    普通方法是对象已经造好之后,用来做事;
    构造器是对象诞生的那一刻,强制完成初始化,防止漏初始化。 重点是强制

注意:

  • 使用new关键字,本质是在调用构造器
  • 用来初始化值
  • 有参构造:一旦定义了有参构造,如果想用无参构造,就必须显示定义无参

关于this

  • this.属性 / this.方法():this = 当前调用方法的对象(实例)
  • this(参数):特殊语法,调用本类里面其他构造器(只能写在构造器首行)

创建对象内存分析

image-20260930183709479

简单小结类与对象

  1. 类与对象 类是抽象,对象是具体

  2. 方法 定义 调用

  3. 对象的引用

    引用类型: 对象通过引用来操作 栈—>堆

    基本类型8大

  4. 属性:字段Field 成员变量 默认初始化

    修饰符 属性类型 属性名=属性值

  5. 对象的创建和使用

    • 必须使用new关键字创建对象 构造器
    • 对象的属性 点操作符
    1. 类:

      静态的属性

      动态的行为

封装详解

  • 该漏的漏,该藏的藏

    程序设计追求“高内聚,低耦合”

    高内聚就是类的内部数据操作细节自己完成,不允许外部干涉

    低耦合就是仅暴露少量的方法给外部使用

  • 封装(数据的隐藏)

    通常,应禁止直接访问一个对象中的数据的实际表示,而应通过操作接口来访问。这称为信息隐藏

  • 属性私有,private,提供一些可以操作这个属性的方法———get/set(提供一些public的get set方法)

  • get获得这个数据 set给这个数据设置值

    1
    2
    3
    4
    5
    6
    public String getName(){
    return this.name;
    }
    public void setName(String name){
    this.name=name;
    }

    封装带来的好处

    • 提高程序的安全性,保护数据
    • 隐藏代码的实现细节
    • 统一接口
    • 系统的可维护增加了

什么是继承

  • 继承的本质:对某一批类的抽象,从而实现对现实世界更好的建模
  • extands的意思是“扩展”,子类是父类的扩展
  • JAVA中类只有单继承,没有多继承
    • 继承是类和类之间的一种关系。除此之外,类和类的关系还有依赖、组合、聚合等
    • 继承关系的俩个类,一个为子类(派生类),一个为父类(基类)。子类继承父类,使用关键字extends来表示
    • 子类和父类之间,从意义上讲应该具有“is a”的关系
  • object类 java中所有的类都默认直接或者间接地继承object

super详解

this:当前实例对象;super:访问父类成员的语法标记,二者指向同一个对象,不存在单独父类对象。

都只能在实例方法 / 构造器使用,静态方法不能用;this()和super()都必须在构造器首行,不能一起写。

super注意点

  • super调用父类的构造方法,必须在构造方法的第一个

  • super必须只能出现在子类的方法或者构造方法中

  • super和this不能同时构造方法

  • vs this

    • 代表的对象不同:

      this:本身调用者这个对象

      super:代表父类对象的引用

    • 前提:

      this:没有继承也可以使用

      super:只能在继承条件下才可以使用

    • 构造方法;

      this():本类的构造

      super():父类的构造

方法重写

与方法重载不一样

重写都是方法的重写,和属性无关

就是子类写了一个和父类一模一样的方法

  • 方法名相同
  • 参数列表相同
  • 返回值类型兼容,协变返回
  • 权限不能比父类更严格

当子类对象调用这个方法时,优先执行子类里的版本,而不是父类的

产生目的:

父类定义一套通用行为,但每个子类的具体实现不一样,如果没有重写机制,只能在子类里写全新名字的方法,代码没法统一调用

设计目标:

同一个方法名,不同对象有不同的实现

统一接口,各自实现

父类规定“能干这件事”,子类自定义“这件事怎么做”

方法重写的硬性规则

  1. 必须发生在继承关系(父类和子类)

  2. 方法名、参数列表必须完全一致

  3. 子类访问权限不能低于父类

    父类public,子类不能写private

    为什么:

    父类对外承诺:我有这个方法,可以被外部调用

    子类是父类的一种,可以当成父类来使用(多态)。

    如果子类把方法改成 private,那用父类引用指向子类对象时,调用这个方法,就会出现:父类说能调用,子类却不让调用,逻辑矛盾。

    1
    2
    3
    4
    5
    6
    7
    8
    //错误代码示例
    class Father{
    public void run(){}
    }
    class Son extends Father{
    // 错误!权限变严格 public → private
    private void run(){}
    }
  4. 返回值:可以是父类返回值的子类类型(协变返回)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Animal{}
    class Dog extends Animal{}

    class Father{
    public Animal getAnimal(){return null;}
    }
    class Son extends Father{
    // 合法,协变返回,Dog是Animal子类 子类返回更具体的类型,调用的时候不用强制类型转换,写代码更方便。
    @Override
    public Dog getAnimal(){return null;}
    }
  5. 父类如果抛出异常,子类不能抛出更大的异常

    为什么:

    外部代码在调用父类方法时,只会捕获父声明的异常。

    如果子类抛出更大的异常,外部代码没有处理,程序直接崩,破坏代码安全。

    运行时异常(RuntimeException)不受这个限制

  6. 用注解@Override,用来校验是不是真的重写(推荐必写)

    没有 @Override 的时候,很容易手滑写错方法名,编译器不会报错,它会当成子类新增了一个独立方法,你以为是重写,实际根本没生效,bug 很难排查。

哪些不是重写

  • 静态方法static:子类写同名静态方法,叫隐藏 hide,不是重写

    静态属于类,不属于对象,不遵循多态。引用是谁,就调用谁的静态方法。

  • 方法参数不一样->方法重载,不是重写

  • 父类private:子类写同名方法,不算重写。父类私有对子类不可见

  • 构造器不能重写

  • 对比 重写vs重载

    • 重写override:父子类之间。方法名、参数相同。运行期决定调用哪个方法(多态)
    • 重载 overloading :同一个类里面,方法名相同,参数不同。编译器就确定调用哪个
  • final常量 不能重写

总结

重写就是:子类对父类已经定义好的行为,重新实现一套逻辑。目的是实现多态,统一调用方式,不同对象有不同行为。

四个访问修饰符

一句话:控制【谁能访问这个类/方法/变量】

范围从大到小:public >protected>default>private 公保默私 访问范围越来越小

Java的package包,本质就是用来分类管理.java文件的文件夹

包名用 .分割:com.animal、com.animal.dog

目录结构:

1
2
3
4
5
6
7
src
└── com
└── animal 包:com.animal
├─ Animal.java
|--Pig.java
└─ cat
└─ Cat.java 包:com.animal.cat
  1. 同类:

    同一个.java里面,同一个类里面的代码

    比如:Animal类里面,写在Animal大括号{}里面的变量/方法

  2. 同包

    文件夹完全一样,包名一模一样

    com.animal.Animal和com.animal.Pig

    两个文件在同一个文件夹animal,包名都是com.animal 就是同包

    不管有咩有继承关系,只要包相同,就是同包

  3. 子包

    com.animal.cat是com.animal的子包(子文件夹)

注意:

  • 子包!=同包

    虽然文件夹是嵌套关系,但是 Java 眼里:com.animal 和 com.animal.cat 是两个完全独立不同的包

  • 跨包

    只要包名不一样,全部叫跨包。

    两种情况都属于跨包:

    1. com.animal 和 com.people(完全无关文件夹)
    2. com.animal 和 com.animal.cat(父子文件夹,子包,也是跨包)
  • 子类

    继承关系:class Dog extends Animal

    Dog 就是 Animal 的子类。

    子类只看有没有extends,和包没关系!

    子类可以:

    • 和父类同包
    • 也可以在别的包(跨包子类)
  1. public

    范围:全项目任何地方都能访问

    对外暴露的API,所有人都能用

    例子. main方法、我们写的对外提供的方法,一般public

  2. protected 受保护

    范围:同包随便用;跨包只有子类能用

    父类有些东西,希望留给子类继承、重写,但是不想开放给项目里其他无关的类。

    坑:跨包的普通类,就算导入了父类,也访问不到 protected 成员,只有子类才行。

  3. default 不写修饰符

    范围:仅限同一个包下的所有类,跨包直接不行,哪怕是子类

    包内的工具类,包内可以互相使用,包外面隔离。

    区分 protected 和 default :

    protected:跨包子类还能用

    default:跨包,哪怕是子类也不能访问!

  4. private 私有

    范围:只能当前这个类里面访问,出了这个类就完全看不见

    封装思想 把类内部的细节隐藏起来,外部不能直接修改。外部只能通过 public 方法来间接操作。

    实体类的成员变量一般都写 private,外部不能直接 obj.age,要用 get/set。

    防止外部随意乱改数据,做校验。

补充:

成员变量、成员方法 4 个修饰符都能用;

外部类(写在.java 文件里的类)只能用 public /default

什么是多态

同一种行为,多种形态

Java多态核心一句话:父类引用,指向子类对象。编译看左边,运行看右边。

1
2
Animal a = new Dog(); // 父类引用 a,指向子类Dog对象
a.shout(); // 运行的时候,执行Dog里面shout()
  • 编译阶段:编译器只看左边 Animal,检查 Animal 有没有 shout 方法。左边没有,直接编译报错。
  • 运行阶段:JVM 看new出来真实对象(Dog),执行子类重写后的方法。

解决痛点

假设我们有:狗、猫、鸟,都继承动物,都有 shout ()。

没有多态的写法:

​ 写一个方法接收狗,一个方法接收猫,一个接收鸟。新增动物,就要新增方法,代码无限膨胀

1
2
void dogShout(Dog d){d.shout();}
void catShout(Cat c){c.shout();}

有了多态之后

只需要一个方法,接收父类 Animal,所有 Animal 子类都能传进去。新增动物类,这个方法完全不用改。

1
2
3
4
5
6
7
8
void makeShout(Animal animal){
animal.shout();
}
//调用:
makeShout(new Dog());
makeShout(new Cat());
makeShout(new Bird())
//设计思想:开闭原则。对扩展开放,对修改关闭。新增子类,不用改动原有业务代码。

多态生效的前提

  1. 要有继承(子类 extends 父类)
  2. 要有重写(子类重写父类方法)
  3. 父类引用指向子类对象 Animal a = new Dog()

多态的坑

  • 成员变量不参与多态

    1
    Animal a = new Dog();

    访问a.name,永远看左边父类的变量。变量不存在重写。

  • static静态方法不参与多态

    静态方法属于类,不是对象。a.staticMethod(),看左边引用类型,不是看 new 出来的对象。

  • 父类引用不能直接调用子类独有的方法

    1
    2
    3
    4
    5
    Animal a = new Dog();
    a.watchHouse(); // 编译报错!watchHouse是Dog独有,Animal没有。
    // 如果一定要调用,必须向下转型:
    Dog d = (Dog) a;
    d.watchHouse();

父类定义通用规范,子类各自实现;同一个父类引用,传入不同子类对象,自动执行子类重写后的逻辑。

关系梳理

继承 → 方法重写 → 向上转型 → 产生多态效果

建立父子关系->重写,具体实现->子类对象交给父类引用接收->触发多态

形象比喻:

父类规定了一个接口:shout()(可以叫)

子类 Dog 重写:shout→汪汪;子类 Cat 重写:shout→喵喵

重写 = 提前录好不同版本的 “叫声”

多态 = 拿着一个通用遥控器(父类引用 Animal),拿到狗就播放汪汪,拿到猫就播放喵喵。

向上转型 统一调度 简化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//不用向上转型
void dogShout(Dog d){d.shout();}
void catShout(Cat c){c.shout();}
void birdShout(Bird b){b.shout();}

//使用向上转型 一个方法
void makeShout(Animal animal){
animal.shout();
}

makeShout(new Dog());//传参本质就是向上转型:Dog/Cat/Bird 子类对象,自动转为 Animal 父类引用。
makeShout(new Cat());
makeShout(new Bird());
/*新增一个 Snake 类,只需要写 Snake 继承 Animal、重写 shout。
makeShout 方法一行代码都不用改!
这就是开闭原则:对扩展开放,对修改关闭。

代价:父类引用看不到子类独有的方法
如果需要调用子类特有功能,就需要 instanceof 判断 + 向下转型。*/

instanceof 和 类型转换

instanceof是判断引用变量指向的真实对象,是不是某个类(或者它子类)的实例

语法:对象引用 instanceof 类名

返回布尔值:true / false

1
2
3
Animal a = new Dog();
System.out.println(a instanceof Dog); // true
System.out.println(a instanceof Animal); // true,Dog是Animal子类

为什么需要:

真实痛点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Animal a = new Dog();
/* a是Animal引用,不能直接调用Dog独有的方法watchHouse()
想要调用,必须向下转型 Dog d = (Dog)a;
但是!如果这个引用实际不是 Dog 对象,强转直接抛异常 ClassCastException,程序崩掉。
所以 instanceof 的诞生目的:转型之前先做安全校验,判断能不能转,防止报错*/

//旧写法:先判断再强转
if(a instanceof Dog){
Dog d = (Dog) a;
d.watchHouse();
}
//Java14+ 模式匹配,简化写法(了解即可)
if(a instanceof Dog d){
d.watchHouse();
}

注意点:

  1. instanceof 判断的是堆里面真实对象的类型,不是左边引用类型!

  2. null 引用 instanceof 任何类,结果都是 false

    1
    2
    Animal a = null;
    System.out.println(a instanceof Dog); // false,不会空指针
  3. 编译期就不可能成立的类型,编译器直接报错

    比如

    1
    String s = "abc"; if(s instanceof Animal)

    直接编译失败。

Java类型重新理解

  1. Java就两大类:基本类型+引用类型

    • 基本数据类型 (8个 固定死) byte short int long float double boolean char

      变量里面直接存的就是真实的值,变量本身就是数据。

      1
      int a = 10;

      栈里的变量 a,里面写的就是数字 10。没有堆、没有指向。

  2. 引用类型(剩下所有全都是!)

    • 自定义类 (我们自己写的Man,Animal,Dog)
    • Java自带类 String,ArrayList
    • 数组 数组也是引用类型!!

    引用类型变量,栈里面存的不是对象本身,是一个地址(引用)

    这个地址指向堆里面那个真实对象

    1
    Dog d = new Dog();

    栈:变量d,存地址

    堆:new Dog()创建出来的 Dog 对象,真实数据在这。

引用变量的【声明类型】和【堆里真实对象类型】可以不一样 多态的根基

1
2
3
4
5
Animal a = new Dog();
/*a 这个变量,它本身是Animal 类型的引用变量。
但它手里的地址,可以指向它的子类对象 Dog。
引用变量的类型,只是一张 “权限卡”;堆里面的对象才是本体。
权限卡写的是 Animal,你只能做 Animal 允许的操作;但本体是 Dog,重写的方法就会跑 Dog 的版本。*/

左边:引用的类型(编译类型,写代码时定义的):Animal

规定:编译器只允许你调用 Animal 里面有的方法。

右边 new 出来:堆里面真实对象(运行时类型):Dog

运行的时候,JVM 看这个真实对象,执行 Dog 重写的方法。

引用变量是什么类型,决定你能调用哪些方法;堆里真实对象是什么类型,决定重写的方法执行哪一套。

  • 区分:

    变量的类型(引用是什么类型)

    对象的类型(堆里面 new 出来是什么)

  • 引用类型独有的特性:引用变量的类型,可以和堆里面真实对象的类型不一样(要有继承关系),这就是多态的底层来源。

封装 继承 多态 抽象类 接口

static关键字详解

  1. 静态变量 类变量
  2. 静态方法 类方法
  3. 静态代码块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Person{
{//2.赋初值
System.out.println("匿名代码块");
}
static{//1.只执行一次
System.out.println("静态代码块");
};
public Person(){//3.
System.out.println("构造方法");
}
public static void main(String[] args){
Person person1=new Person();
System.out.println("================");
Person person2=new Person();
}
}
  1. 静态导入包

    1
    2
    import static java.lang.Math.random;
    import static java.lang.Math.PI;

final修饰类,该类不能被继承

抽象类

  • abstract修饰符可以用来修饰方法也可以修饰类,如果修饰方法,那么该方法就是抽象方法;如果修饰类,那么该类就是抽象类

  • 抽象类中可以没有抽象方法,但是有抽象方法的类一定要声明为抽象类

  • 抽象类,不能使用new关键字来创建对象,它是用来让子类继承的

  • 抽象方法,只有方法的声明,没有方法的实现,它是用来让子类实现的

  • 子类继承抽象类,那么就必须要实现抽象类没有实现的抽象方法,否则该子类也要声明为抽象类

普通类的缺陷:你没法阻止别人创建无意义的父类实例。

abstract 解决上面痛点

1
2
3
abstract class Animal{
abstract void shout(); // 没有代码,空壳方法
}

加了abstract之后,它变成代码层面的半成品:

  1. 编译器强制:不能 new Animal,从根源杜绝创建 “纯粹动物” 这种无意义对象。

  2. 可以定义抽象方法(空壳):强制所有继承它的子类,必须把这个方法写出来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //普通父类:子类可重写、可不重写。
    class Animal{
    void shout(){}
    }
    class Dog extends Animal{
    // 可以不重写shout,代码照样编译通过
    }
    //抽象类:子类必须重写抽象方法,不写直接报错。
    abstract class Animal{
    abstract void shout();
    }
    class Dog extends Animal{
    // 不写shout,直接编译报错!编译器逼你写!
    }

与普通类对比

  • 日常说的类(普通类)的抽象:

    把多个对象的共同特征抽出来,封装成类。是面向对象的思想,所有类都有这个特性。

    特点:类是完整的,可以 new。

  • Java关键字abstract 抽象类

    是语法层面的标记,专门用来:

    • 禁止 new 这个类,它只当模板,只给子类继承
    • 定义抽象方法,强制子类实现某个行为,统一规范
    • 只用来定义规范,不允许单独实例化,并且强制子类必须实现某些方法。

abstract 修饰符

  • 修饰类
    作用:禁止new,只做模板,用来继承

  • 修饰方法

    额外增加一条强制规则,强制子类实现这个对象

思考题

  1. 写一个普通父类,不想让别人 new 它。除了改成 abstract 抽象类,还有别的办法吗?

原理回顾

new 类名()的本质:调用这个类的构造器

所以只要构造器外部访问不到,外面就无法new这个类

1
2
3
4
5
6
7
8
9
10
class Animal{
// 私有构造器,外面不能访问
private Animal(){
}
void shout(){
System.out.println("动物叫");
}
}
// 在别的类里面尝试 new Animal()
Animal a = new Animal(); // 编译报错!构造器private,外部访问不了

私有构造器特点:

  1. 外部不能new

  2. 不能被继承

    子类继承父类的时候,子类构造器默认第一行要调用父类无参构造super()

    如果父类构造器是 private,子类根本调用不到,直接编译报错。

    适合:不想被继承,也不想被 new 的类(比如工具类,Math类就是典型)

  • private构造器:封死实例化+封死继承,工具类用
  • abstract抽象类:封死实例化,但开放继承,用来做多态模板
  1. 抽象类有构造器方法吗

    虽然抽象类不能直接 new,但是它一定有构造器。

    一条继承铁律:子类构造器执行的时候,第一行默认会调用父类的构造器 super()

    • 抽象类是父类,子类继承抽象类,子类创建对象时,必须调用抽象类的构造器,用来初始化抽象类里面的成员变量。

    • 抽象类不能自己 new,但是子类 new 的时候,会去调用抽象类的构造器!

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      abstract class Animal{
      String name;
      //抽象类的构造器
      public Animal(String name){
      this.name = name;
      System.out.println("抽象类Animal的构造器执行了");
      }
      }

      class Dog extends Animal{
      //子类构造器,必须调用父类抽象类的构造器
      public Dog(String name){
      super(name); // 调用抽象类Animal的构造器
      }
      }

      public class Test{
      public static void main(String[] args) {
      Dog dog = new Dog("旺财"); //new子类Dog,触发抽象类构造器执行
      }
      }

      抽象类构造器的唯一用途:子类实例化时,初始化抽象类里面的成员

注意点:

  1. 抽象类里面可以有静态方法,但是:静态方法绝对不能用 abstract 修饰(不能是抽象方法)。
    • 静态方法不依赖实例,和抽象类能不能 new 对象没有冲突。
    • 静态方法属于类,编译阶段就确定调用哪个,不看堆里面 new 出来的对象是谁,只看变量声明类
    • abstract 指望运行时多态重写;static 是编译期绑定,没有多态,二者矛盾,不能共存。
  2. 抽象类里面可以放普通实例方法(不带 abstract,有方法体)。
  3. 普通实例方法(final 修饰的)是已经实现好的,禁止子类修改。
  4. 抽象类只是用来做模板,模板里一部分规则强制子类重写 (abstract 方法),一部分固定逻辑不让子类改 (final 方法)。

复习 抽象类只是串联

  • abstract 修饰类 → 抽象类:不能 new,但可以被继承

  • 抽象类有构造器,子类 new 的时候,通过 super 调用,用来初始化抽象类成员

  • 抽象类可以包含:

    普通成员变量、静态变量、普通实例方法、final 实例方法、静态方法、抽象方法

  • 抽象方法

    abstract void method();:没有方法体,强制子类重写;

  • abstract 不能和 static、private、final 一起用,因为它们都会破坏重写

  • 子类继承抽象类:

    要么实现全部抽象方法,要么子类也标记 abstract

  • 抽象类的诞生目的:

    制作模板父类,允许继承,但禁止直接创建无意义的父类对象;抽象方法强制子类实现规定行为,方便做多态统一调度

接口的定义与实现

  • 普通类:只有具体实现

  • 抽象类:具体实现和规范(抽象方法)都有

    ​ 模板父类,有一部分已经写好代码,留一部分空白让子类补

  • 接口:只有规范

    ​ 纯粹的行为契约,只规定要做什么,完全不关心怎么做

基础语法

1
2
3
4
5
// 定义接口,用interface,不是class
interface Run{
// 抽象方法:只声明,没有方法体
void run();
}
  • 接口不能new,不能创建对象
  • 接口没有构造器!(重点!抽象类是有构造器的,接口完全没有)
  • 类不是 extends 继承接口,而是 implements 实现接口

接口和抽象类最核心的区别

  • 抽象类:is-a 【是一个】的关系

    Animal抽象类,Dog extends Animal → 狗是一种动物

    描述事物本身是什么,可以包含属性、普通方法、构造器。

    只能单继承:一个类只能 extends 一个父类(抽象类也一样,只能继承 1 个)

  • 接口:can-do 【能做某件事】的能力

    Run接口,Dog implements Run → 狗拥有奔跑的能力

    描述一种能力、行为规范,一般不存对象属性。

    一个类可以同时实现多个接口!(多实现,弥补 Java 类只能单继承的短板)

    1
    2
    3
    4
    5
    6
    7
    // 狗既能跑,也能叫
    class Dog implements Run,Shout{
    @Override
    public void run(){}
    @Override
    public void shout(){}
    }

Java8之后接口升级

Java8以前,接口里面只能由抽象方法+常量,完全纯规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 能力包1:会跑
interface RunAble{
void run();
}
// 能力包2:会游泳
interface SwimAble{
void swim();
}

// 狗:是动物,【附加】跑步能力
class Dog extends Animal implements RunAble{
@Override
public void run() {
System.out.println("狗奔跑");
}
}

//鱼:是动物,【附加】游泳能力
class Fish extends Animal implements SwimAble{
@Override
public void swim() {
System.out.println("鱼游泳");
}
}

//人:不是动物,【同时附加跑步+游泳两个能力】
class Human implements RunAble,SwimAble{
@Override
public void run() {
System.out.println("人跑步");
}
@Override
public void swim() {
System.out.println("人游泳");
}
}
//机器人,不是动物,但是可以拥有跑步能力
class Robot implements RunAble{
@Override
public void run() {
System.out.println("机器人奔跑");
}
}

接口就是可复用的能力模块,可以自由贴到任何类上,不受继承体系限制!

把能力抽离出来,脱离类的继承树,跨类复用,统一调度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 会飞的能力接口
interface FlyAble {
// 接口中的常量:默认自带 public static final,写不写都一样
// 含义:飞行最大安全速度上限,固定值,不能修改
public static final int MAX_FLY_SPEED = 900;

// 抽象方法:拥有飞行能力的类,必须实现这个方法
void fly();
}

// 燕子:属于动物,实现飞行能力
class Swallow implements FlyAble {
@Override
public void fly() {
System.out.println("燕子展翅飞翔,燕子的飞行速度不超过" + FlyAble.MAX_FLY_SPEED);
}
}

// 飞机:不是动物,也实现飞行能力
class Plane implements FlyAble {
@Override
public void fly() {
System.out.println("飞机引擎启动起飞,飞机的飞行速度不超过" + FlyAble.MAX_FLY_SPEED);
}
}

public class TestFly {
// 统一调度方法:只要具备FlyAble能力,就能调用起飞
public static void startFly(FlyAble f) {
f.fly();
}

public static void main(String[] args) {
// 访问接口常量:接口名.常量名
System.out.println("规定最大飞行安全速度:" + FlyAble.MAX_FLY_SPEED);

FlyAble swallow = new Swallow();
FlyAble plane = new Plane();
startFly(swallow);
startFly(plane);

// 下面这行直接报错!接口里的是常量,接口里变量默认自带 public static final 不能赋值修改
// FlyAble.MAX_FLY_SPEED = 1000;
}
}

Java8新增:

  • default默认方法:接口里可以带方法体的方法,有默认实现,子类可以选择重写或者直接用
  • static静态方法:接口里面可以放静态方法。属于接口,只能接口名调用,实现类继承不到

image-20261004175111629

N种内部类 (奇葩~)

定义

内部类就是在一个类的内部再定义一个类,不如,A类中定义了一个B类,那么B类相对A类来说就称为内部类,而A类相对B类来说就是外部类了

设计初衷

把强关联、只给外部类用的类,打包封装在一起,不用单独建立.java文件,并且可以访问外部类的私有成员

分类

  • 成员内部类
  • 静态内部类
  • 局部内部类
  • 匿名内部类

详解

成员内部类

  • 写在外部类里面,和成员变量、成员方法平级,属于外部类的实例对象

  • 必须先有外部类对象,才能new内部类对象

  • 可以访问外部类的所有成员,包括private私有属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Outer{
    //外部类私有成员
    private String name = "外部类名字";

    //成员内部类,和方法、变量同级
    class Inner{
    public void show(){
    //直接访问外部类private成员!这是它最大优势
    System.out.println(name);
    }
    }
    }

    public class Test{
    public static void main(String[] args) {
    //写法:外部类对象.new 内部类()
    Outer outer = new Outer();//先new外部例对象
    Outer.Inner inner = outer.new Inner();
    inner.show();
    }
    }

    这个内部类完全是为外部类服务的,外部类之外几乎不会单独使用。内部类可以直接读取外部类私有变量,不用写一堆getter

    典型例子:ArrayList里面的迭代器Itr,就是成员内部类

静态内部类 (也叫嵌套类 static class)

  • 加了static,写在外部类里面,属于外部类本身,不属于外部类实例

  • 不需要外部类对象,直接外部类.内部类new

  • 只能访问外部类的static静态成员,不能直接访问外部实例变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Outer{
    private static String msg = "静态消息";
    private int age = 18; //实例变量

    //静态内部类
    static class StaticInner{
    public void show(){
    System.out.println(msg); //✅可以访问外部static
    //System.out.println(age); //❌报错,不能访问外部实例成员
    }
    }
    }

    public class Test{
    public static void main(String[] args) {
    //不用new外部类!直接new静态内部类
    Outer.StaticInner inner = new Outer.StaticInner();
    inner.show();
    }
    }

    内部类和外部类逻辑绑定,但不需要依赖外部类对象

    经典例子:HashMap里面的Node,就是静态内部类。Node只是存数据,不需要外部HashMap实例

    区分:成员内部类:绑外部对象;静态内部类:绑外部类

局部内部类

  • 定义在方法里面,方法内的类,作用域仅限当前这个方法

  • 出了这个方法,这个类直接消失,外面完全看不见

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Outer{
    public void test(){
    //局部内部类,写在方法内部!
    class LocalInner{
    public void show(){
    System.out.println("局部内部类方法");
    }
    }
    //只能在这个方法里面使用
    LocalInner li = new LocalInner();
    li.show();
    }
    }

    public class Test{
    public static void main(String[] args) {
    Outer o = new Outer();
    o.test();
    // LocalInner 在这里无法使用,出了test方法就失效
    }
    }

    这个类只在当前这一个方法内临时使用,别的地方完全不用。不想单独建文件、不想暴露给整个类,缩小作用域,减少命名污染

    JDK8前,访问方法局部变量,变量必须final

匿名内部类 最常用

  • 没有类名的内部类,一次性使用,用完就没

  • 一般用来实现接口/继承抽象类,只重写里面的方法,只创建这一个对象,不需要复用这个类

    实现接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    interface Animal{//定义接口
    void shout();//抽象方法,要实现
    }
    //情况1:不使用匿名内部类
    // 单独写一个Dog类,实现Animal接口
    class Dog implements Animal{
    @Override
    public void shout() {
    System.out.println("汪汪汪");
    }
    }

    public class Test{
    public static void main(String[] args) {
    Animal dog = new Dog(); // new我们写好的Dog类
    dog.shout();
    }
    }
    /*这个Dog类,就 main 里面只用这一次,后面再也不用。
    还要单独写一个类,占名字,麻烦*/
    //情况2:匿名内部类版本 不用单独定义 Dog 这个命名类,直接 new 的时候当场实现
    public class Test{//测试类
    public static void main(String[] args) {
    // ==========下面这一整块,就是匿名内部类==========
    //匿名内部类:没有类名,直接new接口,写实现
    Animal dog = new Animal(){
    //new 一个实现了 Animal 接口的子类对象,但是这个子类没有名字!
    //创建了一个没有名字的类,implements Animal,然后 new 这个无名类的对象,再向上转型为 Animal 接口引用。
    @Override
    public void shout() {
    System.out.println("汪汪汪");
    }
    };
    dog.shout();
    }
    }

    继承抽象类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    abstract class Animal{
    public abstract void shout();
    }

    public class Test{
    public static void main(String[] args) {
    Animal cat = new Animal(){
    //new 一个继承 Animal 抽象类的无名子类,重写抽象方法。
    @Override
    public void shout() {
    System.out.println("喵喵喵");
    }
    };
    cat.shout();
    }
    }

    匿名内部类的特点:

    • 只有一次使用机会,只能在这里 new 一次,没有类名,别的地方拿不到这个类,不能重复 new。
    • 省去单独创建一个.java文件,或者单独写一个类。
    • 本质:是一个类 + 同时创建这个类的对象,两件事一次性完成。
    • 匿名内部类可以访问方法里的局部变量,但是变量不能二次修改(有效 final)

Error和Exception

什么是异常

​ 实际工作中,遇到的情况不可能是非常完美的。用户输入不一定符合你的要求,你的程序打开某个文件,这个文件可能不存在或者文件格式不对,你要读取的数据,数据可能是空的等。软件程序在运行过程中,非常可能遇到各种异常问题,我们叫异常,Exception,意思是例外。这些,例外情况,或者异常,怎么让我们写的程序做出合理的处理,而不至于程序崩溃。

简单分类

  • 检查性异常
    代表:用户错误或问题引起的异常,这是程序员无法预见的。

    例如要打开一个不存在的文件,一个异常就发生了,这些异常在编译时不能被简单地忽略

  • 运行时异常
    是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略

  • 错误Error:错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。
    例如:当栈溢出时,一个错误就发生了,它们在编译也检查不到的

异常处理框架

  • Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类

  • 在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error和Exception

    image-20261004183353177

image-20261004183945866

捕获和抛出异常

抛出异常

捕获异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.oop.exception;

public class Demo01 {
public static void main(String[] args) {
int a=1;
int b=0;
try{//try监控 ctrl+alt+t 自动包裹
System.out.println(a/b);
}catch (ArithmeticException e){
//catch(想要捕获的异常类型)捕获异常 可以有多个catch,从小到大地捕获
System.out.println("程序出现异常,变量b不能为0");
}finally{//处理善后工作 io流关闭
System.out.println("finally");
}
}
}

image-20261004190201833

异常处理五个关键字 try catch finally throw throws

自定义异常及经验小结

image-20261004190310544

经验小结:

  • 处理运行时异常时,采用逻辑去合理规避同时辅助try -catch 处理
  • 在多重catch块后面,可以加一个catch(Exception)来处理可能被遗漏的异常
  • 对于不确定的代码,也可以加上try-catch,处理潜在的异常
  • 尽量去处理异常,切忌只是简单地调用printStackTrace()去打印输出
  • 具体如何处理异常,要根据不同的业务需求和异常类型去决定
  • 尽量添加finally语句块去释放占用的资源 ,IO Scanner

JavaSE总结

学习的一些思考

1
2
3
4
5
6
7
8
9
弄懂一个概念的7个问题

1.XXX是什么
2.XXX从何而来
3.XXX出现是为了解决什么问题
4.XXX是自上而下推导的,还是自下而上抽象的?是前者由何推导,是后者由何抽象?(如果由技术抽象总结的概念,就去看技术具体实现)
5.XXX为何有效?
6.XXX是历史必然出现的选择吗?为什么
7.XXX如何实现

JAVA面向对象OOP与异常
http://lotus.github.io/2026/09/29/java面向对象OOP与异常/
作者
Lotus
发布于
2026年9月29日
许可协议