说明
这是一个完善了但又不完善的笔记,或许以后会更新
可以参考但请务必超越
源文件
Tools
面向对象编程
面相对象在类和对象里简单讲了一下
从这篇文章开始,就可以逐渐深入了解一些之前见过,在意过或没在意过的问题了
注意,内容理解后都不难,我会非常详细的一步一步的说明解析
包
我们在项目工作中,肯定会遇到和同事写一样类名的情况,必然会起冲突导致代码不能编译通过。这时,只要你和同事把类写在不一样的包里,就可以避免冲突(当然是代码冲突,人可不会起冲突)
包(package)是组织类的一种方式
使用包的主要目的是保证类的唯一性
导入包中的类
我们其实使用过很多Java现成提供的类
我们打印数组使用Arrays
,就会自动在上面导入(import)这个类
可以看到,toString
这个方法是由类名Arrays
直接调用的,说明toString
就是一个静态方法
我们打开toString
看一下
翻到顶部就可以看到
Arrays
这个类是在java.util
包下
那么问题来了
package和import有什么区别呢
package和import
根据上面的内容其实就可以看得出来,2个关键字的后面很像是文件夹的目录
Java提供的Arrays
这个类存储在java.util
目录下,用的时候导入就可以了,那么package
和import
其实就是互逆的关系
package
:将类放到包中
import
:导入包中的类。只能导入具体的类,不能导入包,包里面类多了去了,那总得用一个吧。
Java提供的包和类所在的位置
我们在装jdk
的时候,Java就把现成的一些包以及当中的类存储到的电脑中
通过idea左侧的External Libraries
外部库可以看到有很多现成的包和类
那么Arrays
这个类就在这里
也可以打开电脑的文件夹看一下,当然每个人jdk
的目录不见得一样,所以可以自己看自己的
通过idea也可以打开
idea版本不同界面也会不同,不过都大同小异,只要找Explorer、Show in Explorer、File Path等关键字即可。我这里是较新的21版本,可能就多个Open in
选项
右键你想打开的文件或文件夹,选择Open in
在什么地方打开,也可以看到快捷键
在已打开的文件和左侧的文件都可以右键
压缩包
如果是压缩包,会显示如下3个
- Explorer:explorer.exe是Windows程序管理器或者文件资源管理器
- Directory Path:目录路径
- Terminal:终端,我们使用的命令提示符,PowerShell等就是一种终端,idea运行程序也是在终端窗口
选第一个就直接打开了,如果想看看所在路径可以选第二个
可以看到从下往上依次的路径,点选任何一个都可以Show in Explorer
在资源管理器打开
在终端打开就直接运行
文件夹
如果是文件夹那就只有文件夹的目录路径嘛
文件
如果是个文件那就打开文件目录路径
.jar
.jar
格式是Java的压缩文件格式,就像zip
,rar
,7z
等等
这些压缩文件的后缀名一般都可以互相更改,不过因为压缩方式不一样,不是必要也就不更改
我们使用压缩软件就可以打开
压缩软件就不用我多说了,现在即使没有win10也能打开,不过win10自带的不是很好,曾经都使用WinRAR
,现在又有Bindizip
和其他的软件。用哪个都可以,看个人。不过如果让我推荐,还是Bindzip
或者7-Zip
好。Bindzip
以前没有广告可以用旧版,现在也很好使用,7-Zip
压缩方式也比老的好。倒不是说什么标配,只是前人使用总结过的经验总是有道理的。
打开往下翻就可以看到
我这里使用的是jdk1.8.0_192
的库,当然只要是1.8小版本号无所谓
指定使用包中的类
那现在如果我就不使用import
导包呢?
我们用Date
类实例化一个对象获取时间戳
java.util.Date date = new java.util.Date();
这就相当于指定使用包中的类,后面还会讲
如果只是用一次的话,那倒是也行
关键问题是项目工作中肯定不现实,那某一个类、某一个方法,甚至一个包,要使用很多次
那问题又来了,能不能不写那么多,还要能使用?
通配符
我们直接使用一个*
import java.util.*;
这样就可以使用这个包里的所有类了
那问题又来了,有那么多的类,难道一下就要全部导入吗?
Java处理的时候,需要谁,才会拿谁
C语言通过include
关键字,导入头文件,就会把里面所有的内容全都拿过来。所以这方面当然是Java的好
但是,注意了。虽然使用通配符是省事了,可是也会起冲突。
包里虽然没有同类名的文件
可是不同的包就不一定了
import java.util.*;
import java.sql.*;
现在这2个包里都有Date
类,那咋办嘛,使用Date
必然会出现歧义,编译出错
编译器懵逼:我去我找到了2个Date
类,我要用那个呀
这种情况下,如果非要使用通配符,那就必须使用完整的类名指定编译器用哪个
java.util.Date date = new java.util.Date();
静态导入
一般情况下,静态导入用的非常的少
我们使用的打印方法就是一个静态方法
System.out.println();
使用的是System
包下的方法
import static java.lang.System.*;
导入之后可以不写System
out.println();
虽然很方便,但是看起来怪怪的。直译就是,out
外面,然后换行打印。哪里的外面?
所以一般也不使用静态导入
类似的还有数学的类Math
等
创建一个包
前面说了那么多,都是Java提供现成的包
那我们自己来创建一个包吧
命名规则
包名,必须是小写的
包名需要尽量指定成唯一的名字,通常会用公司域名的颠倒形式
这里又涉及到了域名,那我就简单讲一下
域名
我们看的网页都有唯一的网址对吧
网址就是由域名组成的,具体域名的含义、注册等,可以直接去搜
顶级域名:com
、cn
等等,也叫一级域名。
主域名:字符写在顶级域名前面,使用.
来隔开。也叫二级域名。个人用户只可以申请注册二级域名。例如mywifeasuna.top
。一般主域名就可以直接访问了。
子域名:字符写在主域名前再用.
隔开。二级域名以上级别的域名,统称为子域名,不在“注册域名”的范围中。个人用户只要有主域名就可以随便增加子域名,三级域名...多级域名。
创建目录
idea中右键文件夹创建或者选中文件夹快捷键alt+insert
都可以
一般我们都在src
也就是source源文件夹下创建
注意,如果想要直接分层创建而不是一个文件夹一个文件夹的创建,那么我们需要设置下
在项目的小齿轮里有一个Compact Middle Packages
,取消勾选即可自动分层,用.
分开
创建成功
这时想要把类写在哪里就在那个文件夹创建即可
可以看到package
指定类在那个包下。文件的路径也显示了出来,起同样的类名就不会冲突了
导入一个包
那导入一下我们写的包吧,前面也讲过导入import
问题出现了,可以看到导入的类是灰色的,而且下面还报错了
当然我这里类名都是小写,一般类名都是首字母大写。和这个无关。
使用通配符呢?
不报错了,但还是灰色的,说明下面实例化的对象还是没有用我们包中的类,而是当前的类
解决方法也很简单,就是指定使用包中的类
top.mywifeasuna.www.test test = new top.mywifeasuna.www.test();
包访问权限
首先,包访问权限就是默认(default)。然后访问权限修饰符一共有3个,都是关于类的,当然类一般也都在包里嘛
之前在类和对象中讲到过封装private
,当然还有public
private
只能被类的内部使用,public
只能在包的内部使用
还有一个protected
下面会讲
我们现在先来看包访问权限
在包里新建一个文件,写下一个变量val
public class test1 {
int val = 10;
//默认是包访问权限
}
在同一个包里的另一个类中是可以直接使用的,也就是可以访问
但是在不同的包中就不可以使用
可以看到上面的导入亮了起来,但是下面不能用,无法访问
常见的系统包
java.lang
:系统常用基础类(String、Object),此包从JDK1.1后自动导入。java.lang.reflflect
:java反射编程包。java.net
:进行网络编程开发包。java.sql
:进行数据库开发的支持包。java.util
:java提供的工具程序包。(集合类等) 非常重要java.io
:I/O编程开发包。
诶?java.lang
自动导入,那我们前面静态导入的import static java.lang.System.*
还有Math
等等也是文件夹吗?
这个其实很简单,我们自己翻一下文件就好了
idea中文件的显示
多留心的人肯定会发现,idea对于文件的显示都是不一样的
当idea读取到文件并识别出种类的时候,就会分别同不同的图标显示,比如类文件就是蓝色实心圆中间一个c
继承
面向对象的基本特征前面讲过一个
封装:不必要公开的数据成员和方法,使用private
关键字进行修饰。他的意义在于安全性
现在就来看看另一个,继承
举栗
那我们现在在另一个包里先创建一组动物吧
class Cat {
public String name;
public int age;
public void eat() {
System.out.println("eat");
}
}
class Bird {
public String name;
public int age;
public void eat() {
System.out.println("eat");
}
public void fly() {
System.out.println("fly");
}
}
可以看到他们都有名字和年龄,还都会吃,鸟还会飞
他们都有一些共性,是不是可以就直接写在一个Animal
类里?
extends关键字
extends
意为延长(扩展、拓展),在这里就是继承
使用格式是is - a
class Animal {
public String name;
public int age;
public void eat() {
System.out.println("eat");
}
}
class Cat extends Animal{
// is a
}
class Bird extends Animal{
// is a
public void fly() {
System.out.println("fly");
}
}
就很简单,使用一个extends
继承动物这个类就好了
代码量大大的减少了
父类和子类
也很好理解
Animal
就是父类(基类、超类)
Cat
和Bird
就是子类(派生类)
小结
继承:对共性的抽取。使用extends
关键字进行处理。他的意义在于可以对代码进行重复使用
使用
实例化对象简单赋一些值来看看
可以看到继承父类后,其中的成员属性都可以用
注意(很重要)
- 根据上面的内容,要知道不是所有的动物都会飞,所以要看情况
狗的一生只能飞一次,猫的一生或许能飞九次,运气好能飞十次,运气爆表说不定能飞十一次。 - 看到extends这个格式,可以知道Java当中只能单继承,不能同时继承两个及以上的类。
- 子类会继承父类所有
public
的字段和方法(因为权限嘛)。 - 对于父类的
private
的字段和方法,子类中是无法访问的(因为权限嘛)。可以认为没有被继承 - 子类的实例中,也包含着父类的实例。可以使用
super
关键字得到父类实例的引用。(后面讲super
)
super关键字
上面的第5个注意点,子类的实例中,也包含着父类的实例
其实是说
子类构造的同时,要先帮助父类来进行构造
对呀,那父类得先构造才行呀
那问题就来了,子类怎么帮助父类构造呢?
我们先自己给父类写一个带name
和age
2个参数的构造方法
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
这时我们看一下,下面的2个子类就都报错了
也可以接受嘛,父类的构造方法要求带2个参数,那子类什么都没有
现在我们来写子类的构造方法
public Cat(String name, int age) {
super(name, age);
}
public Bird(String name, int age) {
super(name, age);
}
不报错了
这里使用了一个关键字super
,那其实也很明显了
super
是父类对象的引用
在这里super
调用了父类带有2个参数的构造方法
当然,我们不写构造方法的时候会自动生成嘛
public 类名() {
super();
}
当我们写了带参数的构造方法那自动生成的不就没用了
注意
super
和this
一样,只能出现在当前类的构造方法,且必须在第一行
super
不能出现在静态方法当中
super()
调用父类的构造方法super.func()
调用父类的成员方法super.data
调用父类的成员变量
在类和对象中有写面试题this
和super
有什么区别,现在就有答案了
成员属性同名访问规则
现在我们来看看Bird
我们再增加一个name
,来看看最后会打印哪一个name
可以看到,现在打印的是Bird
中的name
,那就很容易理解了
当父类和子类有同名的成员属性时,会使用子类的成员属性。
这时如果要使用父类的话当然也是使用super
子类在内存中的存储
其实存储这块就没什么太多讲的了,之前也有很多例子
这里的关键点就是父类中private
的成员变量和子类中自己的成员变量
现在我们在父类中添加一个private
在子类Bird
中添加一个翅膀wing
画图来看一下在内存中的存储
protected关键字
现在知道父类里private
子类不能访问,但是如果设置成public
又违背了封装的初衷
所以这个时候就可以使用protected
关键字
关于public
、private
和default
现在是非常明确的,dafault
就是前面的包访问权限嘛
那么先来看一下权限图吧
要研究的点那自然是后两个,不同包中的子类和非子类能不能访问
涉及到子类那当然是要继承
我们在另外一个包的类文件里写一个protected
变量
然后在要继承的包中选择一个类文件继承一下
诶?这不是不能访问吗
其实是访问方式的问题
我们可以这样写
访问是可以访问了,但是下面为什么还是报错了
当然是因为main
是static
静态的...
如果没有继承,不是子类,那自然就无法访问
合理使用
我们希望类要尽量做到”封装“,即隐藏内部实现细节,只暴露出必要的信息给类的调用者。
因此我们在使用的时候应该尽可能的使用比较严格的访问权限。例如如果一个方法能用private,就尽量不要用public。
还有一种简单粗暴的做法,就是所有的字段都设为private
,所有的方法都设为public
。这种方式属于是对访问权限的滥用了,所以还是建议思考下情况,该类提供的字段和方法是给谁用?(类内部?类的调用者?子类?)
默认继承
这里有一个点
我们在创建一个类的时候,如果没有指定继承的父类,那么默认就是继承Object类
也就是说,Object是所有类的父类
如果写全了就是这样
public class test extends Object {
}
//当然一般都是不用去写的
public class test {
}
更复杂的继承关系
动物界,是不是分类大了去了
那如果要分类Animal
是不是一直继承
是...
继承当然可以一直继承,想要多长要多长
这是逻辑上的事情
final关键字
final
修饰常量嘛,这都知道
如果一个类不想被继承,也可以用final
修饰
final
关键字的功能是限制类被继承“限制”这件事情意味着“不灵活”。在编程中,灵活往往不见得是一件好事。灵活可能意味着更容易出错。
使用=用
final
修饰的类被继承的时候,就会编译报错,此时就可以提示我们这样的继承是有悖这个类设计的初衷的。
我们平常使用的String
字符串类就不能被继承
组合
组合其实也是面相对象的一个特征
一般说的最多的是继承封装多态
组合,他是一个a part of
的关系
也没什么特殊语法
前面讲继承是is - a
组合就是has - a
,有一个
这个很容易理解
学校由学生和老师组成
public class Student {
...
}
public class Teacher {
...
}
public class School {
public Student[] students;
public Teacher[] teachers;
}
多态
从字面上理解
多态那就是多种形态嘛。不过这话可不能给面试官说,帮助理解还行
向上转型
我们前面创建的Cat
类
实例化对象是这样的
Cat cat = new Cat("test", 18);
当然Cat
类是子类,父类是Animal
那么现在,我们实例化父类Animal
的对象,让他指向子类实例化对象的引用
Cat cat = new Cat("test", 18);
Animal animal = cat;
这就是向上转型
还可以简写
Animal animal = new Cat("test",18);
父类引用引用子类对象,就是向上转型
发生时机
- 直接赋值 - 我们上面的代码就是直接赋值
- 作为方法的参数:
首先,有父子类关系的两个类
Cat cat = new Cat("test", 18);
Animal animal = new Cat("test",18);
现在有一个方法,参数就是父类Animal
public static void func(Animal animal) {
}
我们可以直接把子类引用cat
作为参数
func(cat);
这也属于向上转型
- 作为返回值
那这也就很好理解了
public static Animal func() {
Cat cat = new Cat("test", 18);
return cat;
}
返回值类型就是Animal
动态绑定
动态绑定(运行时绑定、运行时多态、动态多态)是多态的基础。那么慢慢来讲,还是举栗...
现在已经发生了向上转型
Animal animal = new Cat("test",18);
我们再来看一下父类Animal
和子类Cat
,子类应该是只有一个构造方法
class Animal {
public String name;
public int age;
protected String test;
public void eat() {
System.out.println(name + "正在eat");
}
public void die() {
System.out.println(name + "正在岩浆里游泳");
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
class Cat extends Animal{
public Cat(String name, int age) {
super(name, age);
}
}
通过父类Animal
调用eat
方法,当然肯定会打印test正在eat
那么问题来了,现在子类cat
要津津有味的eat
那在Cat
里面写一下
class Cat extends Animal{
public void eat() {
System.out.println("cat津津有味的eat");;
}
public Cat(String name, int age) {
super(name, age);
}
}
注意,现在去掉了name
,直接写上了cat津津有味的eat。并且没有改变调用方法
animal.eat();
可以看到现在打印的是Cat
,也就是说
动态绑定就是:
当父类引用引用子类对象时,通过父类引用调用父类和子类同名的覆盖方法
也就是:向上转型时,重写
重写
@Override //注释,注解
重写,在类和对象里的Person
类里面,就重写了一个toString
覆盖 覆写 重写:
- 方法名称相同
- 参数列表相同【参数的个数+参数的类型】
- 返回值相同【特殊:返回值也可以是协变类型】
重写又有4个注意点:
- static方法不能重写
- private修饰的方法不能重写
- final修饰的方法不能重写
- 子类方法的访问权限(访问修饰限定符)要大于等于父类的访问权限
运行时绑定
这里有个问题,为什么动态绑定又叫运行时绑定呢?
我们可以来看一下现在写好的类
在文件夹out
里面
我们在此处打开命令提示符、PowerShell或者其他终端
使用反汇编器javap
的-c
命令来对代码进行反汇编(具体命令或许以后会讲)
因为main
在test
里面嘛,那我们就来找一下main
当我们编译好之后,就生成了字节码文件
现在看到编译好后,Animal.eat
还在,那就说明
在编译的时候,不能够确定此时调用谁的方法。在运行的时候,才知道调用那个方法。所以叫做运行时绑定
静态绑定
那有动态绑定,肯定也有静态绑定啦
编译时绑定(静态多态,编译时多态)
那我们直接来研究为什么叫编译时绑定就好了
还记得之前方法有讲过重载
好,那这里就不多讲重载了,我们直接写代码
编译时绑定
class Animal {
public String name;
public int age;
protected String test;
public void eat() {
System.out.println(name + "正在eat");
}
public void die() {
System.out.println(name + "正在岩浆里游泳");
}
public void die(int val) {
System.out.println(name + "运气好,又在岩浆里游泳");
}
public void die(int val, int val1) {
System.out.println(name + "运气爆表!还在岩浆里游泳");
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
现在我们给方法die
写了3个重载方法,分别是没带参数,带一个参数和带2个参数
那我们写3个调用,并反编译看一下
animal.die();
animal.die(1);
animal.die(1,2);
可以看到字节码文件是编译完成后的,所以在编译的时候就已经确定了调用那个方法
也就是说
静态绑定:根据你给的参数类型和个数,推导出调用那个函数。
(编译时绑定:通过函数的重载实现的。编译的时候,会根据你给的参数的个数和类型,在编译期,确定你最终调用的一个方法)
小结(很重要)
动态绑定条件(4个)
- 当父类引用引用子类对象时
- 通过父类引用调用父类和子类同名的覆盖方法(重写)
重写又可以分成3个条件
- 方法名称相同
- 参数列表相同【参数的个数+参数的类型】
- 返回值相同【特殊:返回值也可以是协变类型】
向上转型多态中的父类调用animal.eat
这里简单提一下就好
前面举栗子使用了animal.eat
,因为子类都继承了父类原本有的eat
所以使用父类引用可以调用到
但是如果父类里面没有,比如说Bird
的wing
和fly
那必然是没法调用的
我靠?那我就是要调用呢?
使用向下转型
向下转型
前面向上转型,是父类引用引用子类对象
那向下转型就反过来嘛。
向下转型:子类引用引用父类对象
Animal animal = new Bird("test", 18);
Bird bird = (Bird) animal;
bird.fly();//animal.fly()
这就离谱了,现在父类引用animal
引用的是new
出来的子类Bird
,完了就为了要飞,又把子类引用bird
引用了父类引用animal
。类型不一样还强转了。套娃是吧。
这说明什么,动物是鸟的一个种类是吧?
很扯...
更离谱的还有,如果父类引用引用的是new
出来的子类Cat
Animal animal = new Cat("test", 18);
Bird bird = (Bird) animal;
bird.fly();
只能说离天下之大谱
所以向下转型不是非常的安全(是极度不安全的)
instanceof
如果想安全的使用向下转型,那么就要解决一个引用是否是某个类的实例。
上面父类引用animal
甚至都不是Bird
的实例而是Cat
的实例
所以应该想判断一下
Animal animal = new Bird("test", 18);
if(animal instanceof Bird) {
Bird bird = (Bird) animal;
bird.fly();
}
在构造方法中调用重写的方法(一个坑)
前面在Cat
里重写了一个eat
方法
现在重新new
一个Animal
,但是要在Animal
的构造方法里写一个eat()
public Animal(String name, int age) {
eat();
this.name = name;
this.age = age;
}
public static void main(String[] args) {
Animal animal = new Animal("test", 18);
}
正常打印没有问题
注意,现在如果要重新new
一个Cat
而不用向上转型
Cat cat = new Cat("test", 18);
子类要首先帮助父类构造对吧
class Cat extends Animal{
@Override
public void eat() {
System.out.println("cat津津有味的eat");;
}
public Cat(String name, int age) {
super(name, age);
}
}
结果还是子类重写的eat
多态的好处
现在多态基本上就讲完了,那么前面举了这么多例子,其实就看的出来。
1.类的调用者对类的使用成本进一步降低
- 封装是让类的调用者不需要知道类的实现细节。
- 多态能让类的调用者连这个类的类型是什么都不必知道,只需要知道这个对象具有某个方法即可。
所以,多态可以理解成封装plus版
而且也贴合了《代码大全》中关于“管理代码复杂程度”的初衷
2.能够降低代码的”圈复杂度“,避免使用大量的if - else
这就很好理解了,而多态的好处关键点也在此
我们前面举了那么多的例子,试想一下,如果不用多态,那是不就得用if去判断。好家伙那得写多少才行
什么叫“圈复杂度”?
圈复杂度是一种描述一段代码复杂程度的方式。一段代码如果平铺直叙,那么就比较简单容易理解。而如果有很多的条件分支或者循环语句,就认为理解起来更复杂。
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数,这个个数就称为“圈复杂度”。如果一个方法的圈复杂度太高,就需要考虑重构.不同公司对于代码的圈复杂度的规范不一样。一般不会超过10。
3.可扩展能力更强
也很好理解
对于类的调用者来说,使用多态的方式代码改动成本也比较低。
而对于不用多态的情况,那么多的if - else,改动成本就更高
总结(不是很重要)
多态是面向对象程序设计中比较难理解的部分。我们会在后面的抽象类和接口中进一步体会多态的使用。重点是多态带来的编码上的好处。
另一方面,如果抛开 Java,多态其实是一个更广泛的概念,和“继承”这样的语法并没有必然的联系。
- C++中的“动态多态”和Java的多态类似。但是C++还有一种“静态多态”(模板),就和继承体系没有关系了。
- Python中的多态体现的是“鸭子类型”,也和继承体系没有关系。
- Go语言虽然没有“继承”这样的概念,但也能表示多态。
无论是哪种编程语言,多态的核心都是让调用者不必关注对象的具体类型。这是降低用户使用成本的一种重要方式。
抽象类
我们直接举一个新的栗子,Shape
类。shape就是形状的意思,前面讲多态那就用形状举栗呗
class Shape {
public void draw() {
}
}
现在这个类是有默认的构造方法,然后也没有其他的子类。
那问题来了,前面多态Animal
栗子中的eat
方法在子类Cat
中重写,Animal
和Cat
都会使用各自的eat
方法。现在Shape
也要有一个方法draw
,子类要重写,但是Shape
却用不着这个方法draw
。
也就意味着Shape
可以不实现这个方法吧,不实现那就直接写个分号嘛,只让子类去重写
很明显是有问题的,所以,使用abstract
来修饰
abstract class Shape {
public abstract void draw();
}
一个被abstract
修饰没有具体实现的方法,叫做抽象方法。包含抽象方法的类,也必须使用abstract
修饰,叫做抽象类。
实验
编程嘛,必然是要进行大量的实验验证去帮助学习
既然现在有了抽象类,那就来研究研究
1.抽象类不能直接实例化
我们在idea上直接实例化Shape
,会发现idea自动就帮我们重写好了draw
方法
而且要注意后面还要加分号,所以我们写成一行他就是这样子的
Shape shape = new Shape() { @Override public void draw() { } };
当然,如果不重写的话就报错嘛
2.抽象方法不能被private
封装
改成private
试试
就报错嘛
注意
抽象类的私有成员是有意义的,子类只是不能直接访问嘛
3.抽象类也可以包含和普通类一样的成员和方法
随便添加试试
莫得问题
4.普通类继承抽象类必须重写所有的抽象方法
Shape
用不到,但是子类要用的呀
子类当然就按自己的来,那就重写嘛
一个普通类继承一个抽象类,那么这个普通类当中,需要重写抽象类所有的抽象方法
不重写,那就报错呗
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("○"); }
}
5.通过多态也可以使用抽象类和抽象方法
结合多态,动态绑定,那我们直接使用就好了
写一个方法drawMap
public static void drawMap(Shape shape) {
shape.draw();
}
6.抽象类继承抽象类不用重写抽象方法,但是...
这个就很好理解了,大家都是抽象类嘛...
abstract class Shape1 extends Shape{
public abstract void draw1();
}
注意,如果这时作为继承者的抽象类再次被普通类继承的时候,那么两个抽象类的抽象方法都要被重写
不全部重写那就报错嘛
class Cycle1 extends Shape1 {
@Override
public void draw() {
}
@Override
public void draw1() {
}
}
7.抽象类和抽象方法不能被final修饰
抽象类不能被final修饰,抽象方法当然也不可以被final修饰。他们刚好是互逆的,抽象方法和抽象类就是为了被重写
报错,互相矛盾
抽象类的作用
到此抽象类的作用其实就很简单明了
因为不能被实例化,所以抽象类只能被继承,那么
抽象类最大的作用就是为了继承
抽象类本身不能被实例化,要想使用,只能创建该抽象类的子类。然后让子类重写抽象类中的抽象方法。
那问题就来了,普通类也可以被继承,普通方法也可以被重写,那为啥非要用抽象类和抽象方法呢?
确实如此。但是使用抽象类相当于多了一重编译器的校验
通俗的栗子就是,孙悟空没成佛之前如果不戴紧箍咒,说不定就真会把唐僧干死
也就是说
实际工作应当由子类完成,如果不小心用成父类,对于普通父类编译器是不会报错的。抽象父类就会在实例化的时候提示错误,让我们尽早发现问题
很多语法存在的意义都是为了“预防出错”,例如final也是类似。创建的变量用户不去修改,不就相当于常量嘛?但是加上final就能够在不小心误修改的时候,让编译器及时提醒我们。
充分利用编译器的校验,在实际开发中是非常有意义的。
接口
前面讲抽象类当中,也是可以放其他非抽象方法的。
那么接口,就是抽象类的plus更进一步版本。
接口中包含的方法都是抽象方法,字段只能包含静态常量
前面举栗中其实可以看到idea有显示一个绿色的“I”图标
当我们重写了抽象方法的时候,idea其实就会显示了
那么我们来具体看看接口吧
实现
interface IShape {
public abstract void draw();
}
使用interface来修饰
实验
接口当然也是有语法规则的,那接下来就一个一个看吧
1.接口中的方法
尝试在接口当中写一个普通方法
很明显是不行的,所以这里要用一个关键字default
修饰才可以
还有静态方法,来试一试
当然还有public,我们可以换别的看看
可以看到是不行的,而且默认也都是public,我们删掉public也可以
不是很难,简单小结一下
- 接口当中的普通方法,不能有具体的实现。非要实现就只能通过关键字default来修饰这个方法
- 接口当中可以有static方法
- 里面的所有方法都是public
- 抽象方法默认是public abstract修饰的,在接口中删掉public abstract当然也可以
注意,因为是默认的,所以重写方法的时候,前面必须加上public
权限问题嘛,必须是大于等于。那大于等于public就只能是public
下面的变量也一样
2.接口不可以实例化
- 接口是不可以通过关键字new实例化的
- 类和接口之间的关系是通过implements实现的(相当于继承)
- 当一个类实现了一个接口,就必须要重写接口当中的抽象方法
当然也很好理解,抽象类和接口都不可以实例化
当我们想要使用的话,那就通过implements
来使用接口
抽象方法自然要重写
当然idea也会显示出接口的图标
3.接口的使用
还是动态绑定
public static void main(String[] args) {
IShape iShape = new Cycle();
}
4.接口中的变量
- 接口当中的成员变量,默认是public static final修饰的
自然也可以省略
5.多个接口
实现多个接口继承后面还会细讲,现在先简单实验
- 一个类可以通过关键字extends继承一个抽象类或者普通类,但是只能继承一个。同时也可以通过implements实现多个接口,接口之间使用逗号隔开。
- 接口和接口之间可以使用extends来操作他们的关系。此时意为拓展,拓展功能。如果一个类通过implements实现接口的时候,需要重写包括拓展接口2个接口的方法。
对等关系嘛,接口和接口对等,类和类对等,那继承就用extends。类要使用多个接口,那就用implements实现,当然别忘了重写其中的抽象方法
class Cycle1 implements IShape,IShape1 {
@Override
public void draw() {
System.out.println("○");
}
@Override
public void draw1() {
System.out.println("○");
}
}
接口extends另一个接口,就是拓展了接口的功能
interface A {
public abstract void A();
}
interface B extends A {
public abstract void B();
}
A的功能是A,那B的功能就是AB
当然使用的时候也别忘了重写其中的抽象方法
单继承和多继承
前面讲到类只能继承一个类,我们把他叫做单继承
有的时候我们需要让一个类同时继承多个父类,在某些编程语言中可以通过多继承的方式来实现。然而Java只支持单继承
所以我们可以同时实现多个接口,来达到多继承类似的效果
还是举栗一组动物Animal
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
class Cat extends Animal {
public Cat(String name) {
super(name);
}
}
class Bird extends Animal {
public Bird(String name) {
super(name);
}
}
动物的行为就有很多,但是所有动物的行为都不完全相同
就比如猫就会跑不会飞,鸟就会飞不会跑(简单举例,大部分鸟应该都是蹦跳,但有的鸟,比如鸵鸟确实会跑)。当然猫和鸟肯定都是会吃东西的对吧
那有那么多的行为,如果想写的详细一点,那就是写成类。可是Java是单继承,如果跑、飞、吃都写成类,动物们是没办法继承的
这个时候就可以使用接口了。写成多个接口就可以,根据需求来使用多个接口
idea提供的快捷创建
可别忘了还有idea提供的快捷功能
右键Generate或者Alt+Insert
既然要重写还可以使用ctrl+o,可以看到idea的提示
这里再新说明一个
要快速添加接口,idea可以使用ctrl + i
快速实现接口
世界之大无奇不有,动物界会多种行为的动物多得是
海陆空三栖鸟类:鸭子
interface ISwimming {
void swimming();
}
class Duck extends Animal implements IEating, IFlying, IRunning, ISwimming {
}
ctrl + o
添加父类构造方法
ctrl + i
添加全部行为接口
class Duck extends Animal implements IEating, IFlying, IRunning {
@Override
public void running() {
}
@Override
public void flying() {
}
@Override
public void eating() {
}
@Override
public void swimming() {
}
public Duck(String name) {
super(name);
}
}
非常典型的例子
现在我们来看一下写好的Cat
和Bird
interface IRunning {
void running();
}
interface IFlying {
void flying();
}
interface IEating {
void eating();
}
class Cat extends Animal implements IEating, IRunning {
@Override
public void running() {
System.out.println(this.name + "正在跑!");
}
@Override
public void eating() {
System.out.println(this.name + "正在吃!");
}
public Cat(String name) {
super(name);
}
}
class Bird extends Animal implements IEating, IFlying {
@Override
public void flying() {
System.out.println(this.name + "正在飞!");
}
@Override
public void eating() {
System.out.println(this.name + "正在吃!");
}
public Bird(String name) {
super(name);
}
}
可以看到idea的图标也显示了出来
这就是接口的应用及好处
在这里还可以再举一个非常典型的例子
《Minecraft》我的世界
当玩家自己掉入岩浆死掉,游戏就会自动在对话框中发送“玩家名试图在岩浆里游泳”,也就是name + "试图在岩浆里游泳"
而如果玩家在岩浆里快要死掉的时候,被其他玩家补刀,就会发送“玩家名在逃离玩家名追杀时被岩浆烧死了”,也就是dead + "在逃离" + Killer + "追杀时被岩浆烧死了"
这是非常经典的例子,可以想一下,不同玩家的行为是有很多的,如果仅靠if - else if - else等去判断,那必然会麻烦死,效率和代码量都很头疼
如果使用多个接口,触发那个用哪个,不仅很合适而且还很高级
使用接口
前面讲了那么多东西,现在我们什么都有了,总得用用看吧
我们直接写方法,把接口作为参数
public static void eatFunc(IEating iEating) {
iEating.eating();
}
public static void runFunc(IRunning iRunning) {
iRunning.running();
}
public static void flyFunc(IFlying iFlying) {
iFlying.flying();
}
直接new
对象使用即可
相当的方便
public static void main(String[] args) {
eatFunc(new Cat("咪咪"));
eatFunc(new Bird("啾啾"));
runFunc(new Cat("咪咪"));
flyFunc(new Bird("啾啾"));
}
接口的好处
从上面就可以看出来,使用接口的拓展性(扩展性)非常强
完全不用管方法,引用,只要实现接口就好了
用的时候直接用
继承表达的含义是is - a
,而接口表达的含义是具有什么特性
猫是一种动物,具有会跑的特性。
青蛙也是一种动物,既能跑,也能游泳。
鸭子也是一种动物,既能跑,也能游,还能飞。
这样设计的好处,也就是时刻牢记多态的好处,让程序猿忘记类型。 有了接口之后,类的使用者就不必关注具体类型,而只关注某个类是否具备某种能力即可。
三个常用接口
Java当然也是提供了很多的接口
这里讲一下三个较为有内容的接口
Comparable
排序,那就会涉及到比较
Arrays.sort()
举栗子,现在有一个学生Student
类
class Student {
public int age;
public String name;
public double score;
public Student(int age, String name, double score) {
this.age = age;
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
", score=" + score +
'}';
}
}
通过学生类new
了3个学生
Student[] students = new Student[3];
students[0] = new Student(18, "张三", 70.0);
students[1] = new Student(19, "李四", 80.0);
students[2] = new Student(20, "王五", 90.0);
现在要给他们排序
Arrays.sort(students);
我们直接运行看看
可以看到是有问题的,那问题出在哪?
很明显是没有告诉排序要比较的参数,是根据年龄?姓名?还是分数?
我们来解读一下
这句话的意思是无法强转到Comparable
也就是说有参数要被强转,但是我们没有告诉
这里涉及到底层的东西,我们直接打开sort
来寻找一下
然后打开legacyMergeSort
最后打开mergeSort
可以看到里面的强转(Comparable)
,还有调用了compareTo
最后打开Comparable
可以看到里面只有一个compareTo
重写compareTo
很明显就是通过compareTo
制定规则来排序
虽然不明白<T>
是什么,但是我们可以来使用一下Comparable
接口
class Student implements Comparable<Student>
然后重写compareTo
,通过idea快捷生成
@Override
public int compareTo(Student o) {
return 0;
}
可以看到返回值是int
,要比较嘛,看返回值是大于0、等于0还是小于0
制定规则
@Override
public int compareTo(Student o) {
if (this.age > o.age) {
return 1;
} else if (this.age == o.age) {
return 0;
} else {
return -1;
}
}
//也可以直接返回age相减
@Override
//谁调用compareTo,谁就是this
public int compareTo(Student o) {
return this.age - o.age;
}
注意,这里的this
,那就是谁调用compareTo
谁就是this
嘛
这次再排序看看
public class TestDemo1 {
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student(20, "张三", 70.0);
students[1] = new Student(18, "李四", 80.0);
students[2] = new Student(19, "王五", 90.0);
System.out.println(students[0].compareTo(students[1]));
System.out.println(Arrays.toString(students));
Arrays.sort(students);
System.out.println(Arrays.toString(students));
}
}
注意
Arrays.sort()
是默认从小到大排序
自定义的数据类型进行大小的比较,一定要实现可以比较的接口
改变规则
现在,假设有一天设计这个类的人,抽风了,想改成根据分数排序
@Override
public int compareTo(Student o) {
return (int) (this.score - o.score);
}
那就乱套了,结果自然就变成了根据分数排序
或者想改成根据名字排序
注意,name
是个引用,类型是String
。想要比较就得使用Java提供在String
里面的compareTo
方法。因为一定要用可以比较的接口嘛。
可以看到,Comparable
这样的接口,有很大的缺点:对类的侵入性非常强。一旦写好了,就不敢轻易改动。
试想一下,项目一上线,bug满天飞,回头一调试我草什么问题都没有...
所以,就需要一个更好的方式...
Comparator
我们先把前面写的Comparable
干掉
然后,使用一个比较器Comparator
来比较
假设比较年龄,那就写一个age比较器
class AgeComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
}
现在再来比较看看
AgeComparator ageComparator = new AgeComparator();
System.out.println(ageComparator.compare(students[0], students[2]));
20-8=2
没有任何问题
当然不要忘了排序,只需要把ageComparator
作为参数添加到Arrays.sort()
中即可
Arrays.sort(students, ageComparator);
也可以在Arrays
中找到相对应的sort
因为都是重载嘛,所以有很多
可以看到,是能够传一个Comparator参数的
特点
相较于Comparable
Comparator
就很灵活:对类的侵入性非常弱
注意
Comparable
和Comparator
用哪个接口,取决于你的项目业务,就是具体情况具体分析嘛
一般推荐,都是比较器
Cloneable
现在已知的创建对象的方式只有一个new
,那我们还是先new
一个人出来
class Person {
public int age;
public void eat() {
System.out.println("吃东西。");
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
'}';
}
}
public class Test {
public static void main(String[] args) {
Person person = new Person();
}
}
clone
现在,我们想要克隆一个Person
类,可以调用一个clone()
方法
可以发现这样写是会报错的
快捷键alt+enter
会发现有2个警告要处理
首先第一个,我们可以点开clone()
查看一下
clone()
方法是一个Object
类,也就是说需要强转
那我们自己强转或者点击第一个项,就会自动帮我们强转
可以看到,强转之后,剩下了一个报错
点这一项,却发现会出现一个菜单,那到底是什么意思呢?
implements Cloneable
事实上,要调用clone()
,一个对象想要克隆产生一个副本,那么这个引用所引用的对象,一定是可克隆的
所以,我们要实现一个Cloneable
接口
class Person implements Cloneable
这时,你会发现,没有报错。实现一个接口不是应该重写这个接口中的方法吗,我们打开Cloneable
看一下
所以这里会有一个面试问题:你知道Cloneable接口吗?为啥这个接口是一个空接口?有啥作用?
空接口->标志接口->代表当前这个类是可以被克隆的
这样就很好理解了
- 为什么是空接口,他就是个空接口没有为什么
- 有什么作用?代表当前这个类是可以被克隆的
这时,可以发现clone()
还是在报错
解决问题
呀,现在person.clone()
强转了,Person
也是可克隆的,那为什么还会报错呢?
注意,这里就比较特殊了
Cloneable
虽然是一个空接口,implements不会报错,但是如果这里想要克隆clone()
我们需要重写clone()
这个方法
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
当我们重写后,就不一样了
现在再来看alt+enter
这个clone()
看一下
现在的意思就是,clone()
需要声明一个异常
要么选第一个使用throws
声明,要么选第二个使用try/catch
包裹
关于异常的具体内容,在下下个文章会讲,也很简单。这里就先不讲了,我们直接选第一个
此时,就完全没问题了
副本在内存中的存储
克隆嘛,那就是产生一个一模一样的副本
注意哦,在此处可不能说clone()
方法,或者某一个方法是深拷贝还是浅拷贝
决定深拷贝还是浅拷贝,是代码的实现来绝对,而不是方法的用途,和方法没关系。
此处还可以在Person
中再定义一个引用Money
来验证,每个人多多少少都有点钱嘛。如果不把这个引用也克隆的话那么2个对象就都会指向一个钱。修改方法当然就是在重写clone()
方法中把Money
也克隆一下。这里就不再详细讲述了。
小结
加上Cloneable
现在已知创建对象的方法,就有2个了
小头图版权:《そう、私です。》by CORE 2021年12月6日晚上11点12分 pid:94615586