既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
另外, 针对重写的方法, 可以使用 `@Override` 注解来显式指定.
// Bird.java
public class Bird extends Animal {
@Override
private void eat(String food) {
…
}
}
有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.
我们推荐在代码中进行重写方法时**显式加上** @Override 注解.
>
> 关于注解的详细内容, 我们会在后面的章节再详细介绍.
>
>
> **小结**: 重载和重写的区别.
>
>
>
>
> **体会动态绑定和方法重写**
>
>
> 上面讲的动态绑定和方法重写是用的相同的代码示例.
>
>
> 事实上, 方法重写是 Java 语法层次上的规则, 而动态绑定是方法重写这个语法规则的底层实现. 两者本质上描述的是相同的事情, 只是侧重点不同.
>
>
>
### 3.4理解多态
有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 **多态(polypeptide)** 的形式来设计程序了.我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况.
代码示例: 打印多种形状
class Shape {
public void draw() {
// 啥都不用干
}
}
class Cycle extends Shape {
@Override
public void draw() { System.out.println("○");
}
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("□");
}
}
class Flower extends Shape {
@Override
public void draw() {
System.out.println("♣");
}
}
/我是分割线//
// Test.java
public class Test {
public static void main(String[] args) {
Shape shape1 = new Flower();
Shape shape2 = new Cycle();
Shape shape3 = new Rect();
drawMap(shape1);
drawMap(shape2);
drawMap(shape3);
}
// 打印单个图形
public static void drawShape(Shape shape) {
shape.draw();
}
}
在这个代码中, 分割线上方的代码是 **类的实现者** 编写的, 分割线下方的代码是 **类的调用者** 编写的.
当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并**不知道**, **也不关注**当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为 **多态**
>
> 多态顾名思义, 就是 “一个引用, 能表现出多种不同形态”
>
>
> 举个具体的例子. 汤老湿家里养了两只鹦鹉(圆圆和扁扁)和一个小孩(核弹). 我媳妇管他们都叫 “儿子”. 这时候我对我媳妇说, “你去喂喂你儿子去”. 那么如果这里的 “儿子” 指的是鹦鹉, 我媳妇就要喂鸟粮; 如果这里的 “儿子” 指的是核弹, 我媳妇就要喂馒头.
>
>
> 那么如何确定这里的 “儿子” 具体指的是啥? 那就是根据我和媳妇对话之间的 “上下文”.
>
>
> 代码中的多态也是如此. 一个引用到底是指向父类对象, 还是某个子类对象(可能有多个), 也是要根据上下文的代码来确定.
>
>
> PS: 大家可以根据汤老湿说话的语气推测一下在家里的家庭地位
>
>
>
**使用多态的好处是什么?**
**1) 类调用者对类的使用成本进一步降低.**
* 封装是让类的调用者不需要知道类的实现细节.
* 多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.
因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低.
这也贴合了 <<代码大全>> 中关于 “管理代码复杂程度” 的初衷.
**2) 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else**
例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下:
public static void drawShapes() {
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
String[] shapes = {“cycle”, “rect”, “cycle”, “rect”, “flower”};
for (String shape : shapes) {
if (shape.equals("cycle")) {
cycle.draw();
} else if (shape.equals("rect")) {
rect.draw();
} else if (shape.equals("flower")) {
flower.draw();
}
}
}
如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单.
public static void drawShapes() {
// 我们创建了一个 Shape 对象的数组.
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
new Rect(), new Flower()};
for (Shape shape : shapes) {
shape.draw();
}
}
使用多态前:
![](https://7078909xxh.oss-cn-shanghai.aliyuncs.com/csdn%E5%9B%BE%E5%BA%8A%E4%B8%93%E7%94%A8%E5%8C%BA/202211221851196.png)
使用多态后:
![](https://7078909xxh.oss-cn-shanghai.aliyuncs.com/csdn%E5%9B%BE%E5%BA%8A%E4%B8%93%E7%94%A8%E5%8C%BA/202211221851405.png)
由上面两个图的对比我们可以看出多态使我们代码看起来更加的高级,且大大的优化了我们的代码量。
>
> 什么叫 “圈复杂度” ?
>
>
> 圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
>
>
> 因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”. 如果一个方法的圈复杂度太高, 就需要考虑重构 .
>
>
> 不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10 .
>
>
>
**3) 可扩展能力更强.**
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.
class Triangle extends Shape {
@Override
public void draw() {
System.out.println(“△”);
}
}
对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.
而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高.
### 3.5向下转型 - 子类引用父类对象
向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象. 相比于向上转型来说, 向下转型没那么常见,但是也有一定的用途.
![](https://7078909xxh.oss-cn-shanghai.aliyuncs.com/csdn%E5%9B%BE%E5%BA%8A%E4%B8%93%E7%94%A8%E5%8C%BA/202211221852701.png)
// Animal.java
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(“我是一只小动物”);
System.out.println(this.name + “正在吃” + food);
}
}
// Bird.java
public class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void eat(String food) {
System.out.println(“我是一只小鸟”);
System.out.println(this.name + “正在吃” + food);
}
public void fly() {
System.out.println(this.name + "
正在飞");
}
}
接下来是我们熟悉的操作
Animal animal = new Bird(“圆圆”);
animal.eat(“谷子”);
// 执行结果
//圆圆正在吃谷子
接下来我们尝试让圆圆飞起来
animal.fly();
// 编译出错
//找不到 fly 方法
**注意事项**
编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法.
虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的.
对于 `Animal animal = new Bird("圆圆")` 这样的代码,
* 编译器检查有哪些方法存在, 看的是 Animal 这个类型
* 执行时究竟执行父类的方法还是子类的方法, 看的是 Bird 这个类型.
那么想实现刚才的效果, 就需要向下转型.
// (Bird) 表示强制类型转换
Bird bird = (Bird)animal;
bird.fly();
// 执行结果
//圆圆正在飞
但是这样的向下转型有时是不太可靠的. 例如
Animal animal = new Cat(“小猫”);
Bird bird = (Bird)animal;
bird.fly();
// 执行结果, 抛出异常
//Exception in thread “main” java.lang.ClassCastException: Cat cannot be cast to Bird
//at Test.main(Test.java:35)
animal 本质上引用的是一个 Cat 对象 , 是不能转成 Bird 对象的. 运行时就会抛出异常.
所以, 为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换
Animal animal = new Cat(“小猫”);
if (animal instanceof Bird) {
Bird bird = (Bird)animal;
bird.fly();
}
`instanceof` 可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较安全了.
### 3.6super 关键字
前面的代码中由于使用了重写机制, 调用到的是子类的方法. 如果需要在子类内部调用父类方法怎么办? 可以使用super 关键字.
**super 表示获取到父类实例的引用.** 涉及到两种常见用法.
1. 使用了 super 来调用父类的构造器(这个代码前面已经写过了)
public Bird(String name) {
super(name);
}
2. 使用 super 来调用父类的普通方法
public class Bird extends Animal {
public Bird(String name) {
super(name);
}
@Override
public void eat(String food) {
// 修改代码, 让子调用父类的接口.
super.eat(food);
System.out.println(“我是一只小鸟”);
System.out.println(this.name + “正在吃” + food);
}
}
在这个代码中, 如果在子类的 eat 方法中直接调用 eat (不加super), 那么此时就认为是调用子类自己的 eat (也就是递归了). 而加上 super 关键字, 才是调用父类的方法.
注意 super 和 this 功能有些相似, 但是还是要注意其中的区别.
![](https://7078909xxh.oss-cn-shanghai.aliyuncs.com/csdn%E5%9B%BE%E5%BA%8A%E4%B8%93%E7%94%A8%E5%8C%BA/202211221855767.png)
### 3.7在构造方法中调用重写的方法(一个坑)
一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func
class B {
public B() {
// do nothing
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D(); }
}
// 执行结果
//D.func() 0
* * B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
* 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0.
**结论:** “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.
### 3.8总结
多态是面向对象程序设计中比较难理解的部分. 我们会在后面的抽象类和接口中进一步体会多态的使用. 重点是多态带来的编码上的好处.
另一方面, 如果抛开 Java, 多态其实是一个更广泛的概念, 和 “继承” 这样的语法并没有必然的联系
* C++ 中的 “动态多态” 和 Java 的多态类似. 但是 C++ 还有一种 “静态多态”(模板), 就和继承体系没有关系了.
* Python 中的多态体现的是 “鸭子类型”, 也和继承体系没有关系.
* Go 语言中没有 “继承” 这样的概念, 同样也能表示多态
无论是哪种编程语言, 多态的核心都是让调用者**不必关注对象的具体类型**. 这是降低用户使用成本的一种重要方式.
![](https://7078909xxh.oss-cn-shanghai.aliyuncs.com/csdn%E5%9B%BE%E5%BA%8A%E4%B8%93%E7%94%A8%E5%8C%BA/202211250927984.png)
## 4.抽象类
### 4.1语法规则
在刚才的打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由Shape 的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 **抽象方法(abstractmethod)**, 包含抽象方法的类我们称为 **抽象类(abstract class)**.
abstract class Shape {
abstract public void draw();
}
* 在 draw 方法前加上 abstract 关键字, 表示这是一个抽象方法. 同时抽象方法没有方法体(没有 { }, 不能执行具体代码).
* 对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类.
**注意事项**
1)抽象类不能直接实例化
Shape shape = new Shape();
// 编译出错
//Error:(30, 23) java: Shape是抽象的; 无法实例化
2)抽象方法不能是 private 的
abstract class Shape {
abstract private void draw();
}
// 编译出错
//Error:(4, 27) java: 非法的修饰符组合: abstract和private
3)抽象类中可以包含其他的非抽象方法, 也可以包含字段. 这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用
abstract class Shape {
abstract public void draw();
void func() {
System.out.println("func");
}
}
class Rect extends Shape {
…
}
public class Test {
public static void main(String[] args) {
Shape shape = new Rect();
shape.func();
}
}
// 执行结果
//func
4)包含抽象方法的类,叫抽象类
5)什么是抽象方法,一个没有具体实现的方法,被abstract修饰。
6)因为抽象类不能被实例化,所以这个抽象类,其实只能被继承
7)抽象类当中,也可以包含和普通类一样的成员和方法
8)一个普通类,继承了一个抽象类,那么这个普通类当中,需要重写这个抽象的所有的抽象方法。
9)抽象类最大的作用就是为了被继承
10)一个抽象类A,如果继承了一个抽象类B,那么这个抽象类A,可以不实现抽象父类B的抽象方法。
11)结合第十点,当A类 再次被一个普通类继承后,那么A和B这两个抽象类当中的抽象方法,必须被重写。
### 4.2抽象类的作用
抽象类存在的最大意义就是为了被继承.
抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.
>
> 有些同学可能会说了, 普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢?
>
>
>
确实如此. 但是使用抽象类相当于多了一重编译器的校验
使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题.
>
> 很多语法存在的意义都是为了 “预防出错”, 例如我们曾经用过的 final 也是类似. 创建的变量用户不去修改, 不就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们.
>
>
> 充分利用编译器的校验, 在实际开发中是非常有意义的.
>
>
>
## 5.接口
>
> 因为我们如果要定义一个鸭子它能飞能跑还能游泳,如果继承类的话就只能继承一个,且不是所有的动物都能飞、跑和游泳,但是如果我们继承接口的话,我们可以继承三个不同的接口(飞,跑,游泳)来实现我们不同的功能,由此便达到了我们的目的,接口便也诞生了!
>
>
>
1、使用interface来修饰的。interfece IA {}
2、接口当中的普通方法,不能有具体的实现。非要实现,只能通过关键字default来修饰这个方法。
3、接口当中,可以有static的方法。
4、里面的所有的方法都是public的。
5、抽象方法默认是public abstract的。
6、接口是不可以被通过关键字new来实例化的。
7、类和接口之间的关系是通过implements来进行实现的。
8、当一个类实现了一个接口,就必须要重写接口当中的抽象方法。
9、接口当中的成员变量,默认是public static final修饰的。
10、当一个类实现一个接口之后,重写这个方法的时候,这个方法前面必须加上public
11、一个类可以通过关键字extends继承一个抽象类或者一个普通类,但是只能继承一个类,同时也可以通过implements实现多个接口,接口之间使用逗号隔开就好。
12、接口和接口之间 可以使用extends来 操作他们的关系,此时,这里面意为:拓展。一个接口 通过 extends来 拓展另一个接口的功能。此时当一个类D 通过implements实现这个接口B的时候,此时重写的方法不仅仅是B的抽象方法,还有他从C接口,拓展来的功能[方法]。
接口是抽象类的更进一步. 抽象类中还可以包含非抽象方法, 和字段. 而接口中包含的方法都是抽象方法, 字段只能包含静态常量.
### 5.1语法规则
在刚才的打印图形的示例中, 我们的父类 Shape 并没有包含别的非抽象方法, 也可以设计成一个接口
interface IShape {
void draw();
}
class Cycle implements IShape {
@Override
public void draw() {
System.out.println(“○”);
}
}
public class Test {
public static void main(String[] args) {
IShape shape = new Rect();
shape.draw();
}
}
* 使用 interface 定义一个接口
* 接口中的方法一定是抽象方法, 因此可以省略 abstract
* 接口中的方法一定是 public, 因此可以省略 public
* Cycle 使用 implements 继承接口. 此时表达的含义不再是 “扩展”, 而是 “实现”
* 在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例.
* 接口不能单独被实例化.
>
> 扩展(extends) vs 实现(implements)
>
>
> 扩展指的是当前已经有一定的功能了, 进一步扩充功能.
>
>
> 实现指的是当前啥都没有, 需要从头构造出来.
>
>
>
接口中只能包含抽象方法. 对于字段来说 , 接口中只能包含静态常量(final static).
interface IShape {
void draw();
public static final int num = 10;
}
其中的 public, static, final 的关键字都可以省略. 省略后的 num 仍然表示 public 的静态常量.
>
> 提示:
>
>
> 1. 我们创建接口的时候, 接口的命名一般以大写字母 I 开头.
> 2. 接口的命名一般使用 “形容词” 词性的单词.
> 3. 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性.
>
>
>
**一个错误的代码:**
interface IShape {
abstract void draw() ; // 即便不写public,也是public
}
class Rect implements IShape {
void draw() {
System.out.println(“□”) ; //权限更加严格了,所以无法覆写。
}
}
接口可以不加public(因为会默认为public),但是子类继承接口后必须要带上public,因为如果子类权限低于接口则idea会报错。
![](https://7078909xxh.oss-cn-shanghai.aliyuncs.com/csdn%E5%9B%BE%E5%BA%8A%E4%B8%93%E7%94%A8%E5%8C%BA/202211221901768.png)
### 5.2实现多个接口
有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 **多继承** 的方式来实现的.
然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果.现在我们通过类来表示一组动物.
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
另外我们再提供一组接口, 分别表示 “会飞的”, “会跑的”, “会游泳的”.
interface IFlying {
void fly();
}
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
接下来我们创建几个具体的动物
猫, 是会跑的.
class Cat extends Animal implements IRunning {
public Cat(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + “正在用四条腿跑”);
}
}
鱼, 是会游的.
class Fish extends Animal implements ISwimming {
public Fish(String name) {
super(name);
}
@Override
public void swim() {
System.out.println(this.name + “正在用尾巴游泳”);
}
}
青蛙, 既能跑, 又能游(两栖动物)
class Frog extends Animal implements IRunning, ISwimming {
public Frog(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + “正在往前跳”);
}
@Override
public void swim() {
System.out.println(this.name + “正在蹬腿游泳”);
}
}
>
> 提示, IDEA 中使用 ctrl + i 快速实现接口
>
>
>
还有一种神奇的动物, 水陆空三栖, 叫做 “鸭子”
class Duck extends Animal implements IRunning, ISwimming, IFlying {
public Duck(String name) {
super(name);
}
@Override
public void fly() {
System.out.println(this.name + “正在用翅膀飞”);
}
@Override
public void run() {
System.out.println(this.name + “正在用两条腿跑”);
}
@Override
public void swim() {
System.out.println(this.name + “正在漂在水上”);
}
}
上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口.
继承表达的含义是 `is - a` 语义, 而接口表达的含义是 具有 `xxx` 特性
>
> 猫是一种动物, 具有会跑的特性.
>
>
> 青蛙也是一种动物, 既能跑, 也能游泳
>
>
> 鸭子也是一种动物, 既能跑, 也能游, 还能飞
>
>
>
这样设计有什么好处呢? 时刻牢记多态的好处, 让程序猿**忘记类型**. 有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力.
例如, 现在实现一个方法, 叫 “散步”
public static void walk(IRunning running) {
System.out.println(“我带着伙伴去散步”);
running.run();
}
在这个 walk 方法内部, 我们并不关注到底是哪种动物, 只要参数是会跑的, 就行
Cat cat = new Cat(“小猫”);
walk(cat);
Frog frog = new Frog(“小青蛙”);
walk(frog);
// 执行结果
//我带着伙伴去散步
//小猫正在用四条腿跑
//我带着伙伴去散步
//小青蛙正在往前跳
甚至参数可以不是 “动物”, 只要会跑!
class Robot implements IRunning {
private String name;
public Robot(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(this.name + "正在用轮子跑");
}
}
Robot robot = new Robot("机器人");
walk(robot);
// 执行结果
//机器人正在用轮子跑
### 5.3接口使用实例
刚才的例子比较抽象, 我们再来一个更能实际的例子.
**给对象数组排序**
给定一个学生类
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
}
再给定一个学生对象数组, 对这个对象数组中的元素进行排序(按分数降序).
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
this.name = name;
}
@Override
public void run() {
System.out.println(this.name + “正在用轮子跑”);
}
}
Robot robot = new Robot(“机器人”);
walk(robot);
// 执行结果
//机器人正在用轮子跑
### 5.3接口使用实例
刚才的例子比较抽象, 我们再来一个更能实际的例子.
**给对象数组排序**
给定一个学生类
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
}
再给定一个学生对象数组, 对这个对象数组中的元素进行排序(按分数降序).
[外链图片转存中…(img-VrnA2n8L-1715678417287)]
[外链图片转存中…(img-OPN7Dzry-1715678417287)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!