一、接口
1.1 接口的成员的冲突问题
1、父类与父接口的成员变量的冲突问题
package com.test.jiekou;
public interface Flyable {//父接口
int speed = 100;//它前面其实省略了public static final
}
package com.test.jiekou;
public class Animal {
int speed = 200; //前面没有修饰符,就是没有的。
}
冲突解决:
- 调用父类:super.成员变量
- 调用父接口:接口名.成员变量
package com.test.jiekou;
public class Bird extends Animal implements Flyable{
// int speed = 300;
public void test(){
System.out.println("父类的speed = " + super.speed);
System.out.println("父接口的speed = " + Flyable.speed);//因为speed在接口中是static
}
}
2、父类的方法与父接口的默认方法冲突
当父类的方法,与父接口的默认方法冲突(方法签名相同),此时遵循亲爹优先原则。默认子类选择的是父类的方法。
所谓的方法签名:
【修饰符】 返回值类型 方法名(形参列表)
3、两个父接口的默认方法冲突
当两个父接口的默认方法冲突(方法签名相同),子类必须做出选择,否则编译不通过。
需要重写:
- 选择父接口1的:父接口1.super.默认方法
- 选择父接口2的:父接口2.super.默认方法
- 可以完全重写,任何一个父接口都不选
1.2 经典接口:比较器接口
1.2.1 java.lang.Comparable接口
Comparable接口:自然比较接口
应用场景:当两个对象要比较大小或排序时,就需要实现这个接口。
具体方式:哪个类的对象要比较大小,就让哪个类实现Comparable接口。实现接口,必须重写/实现接口的抽象方法。
int compareTo(Object other) 抽象方法
子类/实现类在重写这个方法时,方法体没有限制,但是返回值有具体要求:
- 当this对象 “大于” other对象时,就返回 正整数
- 当this对象 “小于” other对象时,就返回 负整数
- 当this对象 “等于” other对象时,就返回零
结论:Java中凡是涉及到对象比较大小的类型,统统都会实现Comparable接口,并且重写int compareTo(Object obj)方法。
这样才能使用我们后面Arrays.sort等通用方法。
例如:String字符串,Integer整数类等等都实现了这个接口
二、多态(重要)
2.1 什么是多态?
多态的字面意思:多种形态。
Java中多态,是和方法有关。
Java中如何体现多态?
-
编译时:方法的重载,一个功能可以有多种实现的方式,方法名相同,形参列表不同,例如:max方法都是找最大值,但是有多种形式,可以是2个整数,3个整数,2个小数.....
- int max(int a, int b)
- int max(int a,int b, int c)
- double max(double a, double b)
-
运行时:方法的重写,父类或父接口的方法,在不同子类中都有不同的实现。例如:Comparable接口的int compareTo(Object obj),每一个实现类都有不同的实现
- Student类实现Comparable接口,重写compareTo方法,按照成绩比较大小
- Employee类实现Comparable接口,重写compareTo方法,按照薪资比较大小
2.2 运行时多态如何表现?
多态引用:左边是父类或父接口的类型的变量,右边是子类的对象,这种引用方式就是多态引用。
一旦多态引用,就只能调用父类有的公共的方法,不能调用子类特有的方法。
编译时看左边,运行时看右边。如果子类重写了父类/父接口的方法,执行的是子类重写的方法体。如果没有重写,仍然执行父类/父接口的方法。
运行时多态让Java的方法实现动态绑定机制。
2.3 多态应用的场景
2.3.1 多态数组
元素的类型声明为父类类型,元素存储的是子类对象。通过多态数组,可以管理一组具有相同父类的子类对象。
package com.test.duotai;
public class TestArray {
public static void main(String[] args) {
//需求:存储一组图形的对象,统一管理它们,比如:让它们以面积从小到大排序,暂时先不用Comparable接口
//多态数组:元素的类型声明为父类类型,元素存储的是子类对象。
Shape[] arr = new Shape[5];
//下面这些赋值语句,体现了多态引用
//arr[下标] 它们声明的类型是Shape类型,父类类型
//arr[下标] 它们赋值的/存储的是子类对象
//左边是父类或父接口的类型的变量,右边是子类的对象,这种引用方式就是多态引用。
arr[0] = new Circle(2.5);
arr[1] = new Rectangle(4,3);
arr[2] = new Rectangle(5,2);
arr[3] = new Triangle(3,4,5);
arr[4] = new Triangle(6,6,6);
//当arr[下标]调用方法时,就会遵循编译时看左边,运行时看右边
for(int i=1; i<arr.length; i++){
for(int j=0; j<arr.length-i; j++){
//按照面积比较大小
//编译时,arr[j]是Shape类型,会看Shape类中有没有area()方法
//运行时,arr[j]会执行子类中重写的area()方法,具体执行哪个子类的,看arr[j]存的是哪个子类的对象
//遵循了动态绑定
if(arr[j].area() > arr[j+1].area()){
Shape temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
2.3.2 多态参数
方法的形参是父类类型,调用方法时的实参是子类对象。在当前方法中,通过形参调用方法,编译时看父类,运行时看子类。
package com.test.duotai;
public class TestAnimal {
public static void main(String[] args) {
Dog d = new Dog();
Cat c = new Cat();
look(d); //调用方法会有传参的过程 Animal animal形参 = d实参; 比较隐晦的多态引用形式
look(c);
}
//定义一个方法,可以观察动物吃东西的行为
public static void look(Animal animal){//形参是父类类型
animal.eat();
//编译时animal以Animal类型呈现,即以父类类型呈现,只能调用父类有的方法
//运行时animal执行哪个类eat方法,要看具体的实参
}
//可以通过重载的方式,为每一个子类都单独定义一个方法,比较麻烦,重复度高,而且后期维护的成本比较高,如果增加子类,减少子类,这些方法都要涉及到修改
/* public static void look(Dog dog){
dog.eat();
}
public static void look(Cat cat){
cat.eat();
}*/
}
2.3.3 多态返回值
方法的返回值类型是父类类型,实际返回的结果是子类的对象。
package com.test.duotai;
public class TestAnimal2 {
public static void main(String[] args) {
Animal a = buy("狗");//多态引用
//a是Animal类型,但是实际返回的是子类Dog的对象
a.eat();
//编译时看Animal的eat方法
//运行时看Dog的eat方法
a = buy("猫");
a.eat();
//编译时看Animal的eat方法
//运行时看Cat的eat方法
}
//定义一个方法,可以购买不同宠物对象,形参是用来确定宠物的类型
//type是狗,返回Dog的对象
//type是猫,返回Cat的对象
public static Animal buy(String type){
switch (type){
case "狗" : return new Dog();
case "猫" : return new Cat();
default: return null;
}
}
}
2.4 向上转型与向下转型
回忆:基本数据类型也有类型转换
- 自动类型转换:小->大 byte->short->int ->long->float->double
- 强制类型转换:大->小 double->float->long->int->short->byte ,有风险,可能损失精度或溢出截断
现在:向上转型与向下转型是针对引用数据类型,而且是针对父子类(父可以是父类,父接口)
- 向上转型:让子类对象以父类或父接口的类型处理(这里的处理,就是接下来调用方法等操作)
- 向下转型:让父类的变量以子类类型处理(这里的处理,就是接下来调用方法等操作)
2.4.1 向上转型
package com.test.cast;
import com.test.duotai.Animal;
import com.test.duotai.Dog;
public class TestUpCastingAndDownCasting {
public static void main(String[] args) {
Dog d = new Dog();
d.watchHouse();//Dog自己定义的方方法
d.eat();//Animal就有的方法
System.out.println(d.toString());//Object类有的方法
Animal a = d;
//a.watchHouse();//错误
a.eat();
System.out.println(a.toString());
Object o = d;
// o.watchHouse();
// o.eat();
System.out.println(o.toString());
//越向上转型,能调用的方法越少
}
}
2.4.2 向下转型
package com.test.cast;
import com.test.duotai.Animal;
import com.test.duotai.Dog;
public class TestUpCastingAndDownCasting2 {
public static void main(String[] args) {
Object o = new Dog();
// o.watchHouse();
// o.eat();
System.out.println(o.toString());
Animal a = (Animal) o;//强制向下转型,有风险隐患
//a.watchHouse();//错误
a.eat();
System.out.println(a.toString());
Dog d = (Dog) o;
d.watchHouse();//Dog自己定义的方方法
d.eat();//Animal就有的方法
System.out.println(d.toString());//Object类有的方法
//越往下,能调用的方法越多
}
}
2.4.3 为什么有向上转型和向下转型?
当我们使用多态数组、多态参数、多态返回值类型时,不得不向上,因为那个时候,关注的是它们(各个子类)的共同操作。
因为向上转型之后,就失去了调用子类特有方法的能力,如果又需要调用子类特有方法,此时就必须向下转型。
向上转型都是安全的,但是向下转型时会有风险,可能会发生ClassCastException类型转换异常。
2.4.4 关键字:instanceof
instanceof的作用是用于判断某个变量/对象是不是属于某个类型的。
变量/对象 instanceof 类型
当这个变量或对象是该类型或该类型的子类对象时,才会返回true。
package com.test.cast;
import com.test.duotai.Animal;
import com.test.duotai.Cat;
import com.test.duotai.Dog;
public class TestInstanceof {
public static void main(String[] args) {
Object o = new Dog();
//o实际对象的类型是Dog,称为o的运行时类型是Dog类型
//o的编译时类型是Object
//编译时看左边,运行时看右边
//只要Dog或Dog以上的,都是true
System.out.println(o instanceof Object);//true
System.out.println(o instanceof Animal);//true
System.out.println(o instanceof Dog);//true
System.out.println(o instanceof Cat);//false
System.out.println(o instanceof Husky);//false
}
}
2.5 变态面试题
2.5.1 多态引用结合静态方法
结论:没有“编译时看左边,运行时看右边”,因为静态方法不会被重写。
如果是自己写,千万不要用“对象.静态方法”,而是要用“类名.静态方法”
2.5.2 多态引用结合成员变量
2.5.3 虚方法
回忆名词:
静态方法、
非静态方法 / 实例方法
抽象方法
默认方法(只有接口中)
私有方法
本地方法(native)
什么是虚方法?
可以被重写的方法,称为虚方法。
虚方法的调用原则:
- 编译时:看左边
- 用实参的类型与形参列表去匹配,如果有最匹配的,优先考虑最匹配的,如果没有最匹配,找可以兼容的。要是没有可以兼容的,就报错
- 运行时:看右边
- 看子类中是否有对
刚刚匹配的方法进行了重写,有重写的,就执行重写的方法体,没有重写的,仍然执行刚刚匹配的方法的方法体。
- 看子类中是否有对
三、关键字总结
3.1 this关键字
this:代表当前对象,它相当于一个变量,里面是当前对象的首地址。
this可以出现在构造器、实例方法中,不可以出现在静态方法中。
this的用法:
(1)this.成员变量
当局部变量(形参也是局部变量)与成员变量(一般是实例变量)重名,可以用this.成员变量进行区分。
(2)this.成员方法()
它没有必须用的场景,用不用this.都一样。
(3)this() 或 this(参数)
调用本类的无参和有参构造,必须在构造器的首行。
原则:this先从本类开始找,如果本类找不到,自然会去父类。当然也是只能找父类非私有的。
3.2 super关键字
super:代表引用父类的xx,super不可以独立使用。
super:只能出现在子类中,而且只能出现在子类的构造器、实例方法中,也不能出现在静态方法中。
前提:通过super调用父类的某个成员,这个成员不能是private。
super的用法:
(1)super.成员变量
当子类的成员变量与父类的成员变量(一般是实例变量)重名,可以用super.父类的成员变量进行区分。
不建议父子类成员变量重名。
(2)super.成员方法()
当子类重写了父类的某个方法后,又想要在子类中调用父类被重写的方法时,需要加“super.父类的成员方法”。
(3)super() 或 super(参数)
调用直接父类的无参和有参构造,必须在构造器的首行。
(4)当子类中要调用父接口的默认方法时,可以用 “接口名.super.默认方法()”
原则:super直接从父类开始找,不找本类的。当然也是只能找父类非私有的。
3.3 instanceof的模式匹配
四、内部类
4.1 什么是内部类?
定义在另一个类里面的类,称为内部类。外面这个类通常被称为外部类。
4.2 内部类有几种形式?4种
- 成员内部类:
- 位置:类中方法外
- 分为:静态成员内部类 和 非静态成员内部类
- 局部内部类:
- 位置:方法内部
- 分为:有名字的局部内部类 和 匿名的局部内部类
public class Outer{
class One{ //成员内部类,没有static,所以是非静态成员内部类
}
static class Two{//成员内部类,有static,所以是静态成员内部类
}
public void method(){
class Three{ //有名字的局部内部类
}
new Object(){ //匿名内部类
};
}
}
4.3 匿名内部类
声明匿名内部类的语法格式:
new 父类名(){//这个匿名内部类继承了这个父类,()空着表示子类的构造器中调用了父类的无参构造
//类的成员:成员变量、成员方法等
}
new 父类名(实参列表){//这个匿名内部类继承了这个父类,()不是空着表示子类的构造器中调用了父类的有参构造
//类的成员:成员变量、成员方法等
}
new 父接口名(){ //这个匿名内部类实现了这个接口,()空着表示调用的是Object()的无参构造,因为接口没有构造器
//类的成员:成员变量、成员方法等
}
因为匿名内部类没有名字,所以必须在声明类的同时,就把对象创建好。而且这个类只有唯一的对象。
案例1:匿名内部类继承父类
package com.test.inner.anonymous;
public abstract class Father {//抽象类
public abstract void method();//抽象方法
public void show(){//非抽象方法
System.out.println("fff");
}
}
package com.test.inner.anonymous;
public class TestFather {
public static void main(String[] args) {
//定义一个匿名内部类继承Father
Father f = new Father(){
@Override
public void method() {//重写抽象父类的抽象方法
System.out.println("aaa");
}
};
//声明的语句是多态引用,f是父类Father的类型,=右边是Father的匿名子类的对象
f.method();//遵循动态绑定,编译时看左边,运行时看右边重写的代码
f.show();
new Father(){
@Override
public void method() {
System.out.println("ccc");
}
}.method();
//这句语句,是匿名内部类的匿名对象在调用method方法
}
}
案例2:匿名内部类实现接口
package com.test.inner.anonymous;
public interface Flyable {
void fly();//抽象方法,省略public abstract
}
package com.test.inner.anonymous;
public class TestFlyable {
public static void main(String[] args) {
Flyable f = new Flyable() {
@Override
public void fly() {
System.out.println("我要飞的更高!");
}
};
f.fly();
new Flyable(){
@Override
public void fly() {
System.out.println("我要借助风的力量飞上云霄!");
}
}.fly();
}
}
4.4 匿名内部类的应用场景之一:比较器
4.4.1 回忆:Comparable接口
当某个类的对象要比较大小或排序,就可以让这个类实现Comparable接口。
Comparable接口:自然比较接口,通常都是优先考虑它。
抽象方法:int compareTo(Object obj),在重写这个抽象方法的时候,比较大小的两个对象是this 和obj。
因为Comparable接口是要比较大小的对象的类本身实现的接口。例如:Student对象要比较对象,学生对象1.compareTo(学生对象2),学生对象1是this,学生对象2是obj
4.4.2 补充:Comparator接口
Comparator接口:定制比较接口,或者备选的比较接口。只有在Comparable接口不能解决我们问题的情况下,才考虑它。
抽象方法:int compare(Object o1, Object o2),在重写这个抽象方法的时候,比较大小的两个对象是o1和o2。
因为Comparator接口是其他类(这个类可以有名字,也可能没名字)实现,所以在compare方法中的this是Comparator接口的实现类对象本身,不是要比较大小的对象。
4.4.3 答疑:
问题:有名字的类与匿名的类目前看,没有省略什么代码,为什么要用匿名的类?
- 当某个接口的实现类,如果有多个的时候,那么用有名字的类,就会有多个的xxx.java文件,.java文件越多,维护的复杂度就越高
- 以Comparator接口为例,如果有多个类似(但又不相同)的实现类的时候,取名字是一个比较难的工作
- 这个接口的实现类只在某个地方使用一次,那么用独立的.java文件的话,聚合性不够强。而采用匿名内部类可以满足 高内聚的开发原则。避免了在.java文件之间来回切换跳转。
- 新的语法Lambda表达式,它可以大大的简化匿名内部类的写法,就可以看成用匿名内部类会比用有名字的类省事,代码更简洁
4.5 局部内部类(了解)
package com.test.inner.local;
public class TestOuter {
private static int num;//成员变量,静态变量
public static void main(String[] args) {
String info = "测试";//局部变量
// info = "test";
//用有名字的局部内部类继承Father类
class Son extends Father{
@Override
public void method() {
System.out.println("使用外部类的静态变量:" + num);
System.out.println("使用外部类的方法的局部变量:" + info);
}
}
//有名字的类的好处,
// (1)和匿名内部类比,可以创建它多个对象
// (2)与普通的非内部类比,可以直接使用外部类的私有成员
//(3)与成员内部类的区别,可以使用当前方法的局部变量。如果外部类方法的局部变量被局部内部类使用了,这个局部变量就会自动加final.
//这个final在JDK8之前,需要手动添加,JDK8之后,自动添加
Son s1 = new Son();
Son s2 = new Son();
s1.method();
s2.method();
}
}
4.6 成员内部类
问:什么时候用静态成员内部类,什么时候用非静态成员内部类?
原则:如果要在成员内部类中,访问外部类的非静态成员(通常是实例变量或实例方法),那么就只能用非静态成员内部类。
问:内部类能用外部类的私有成员吗?
答:可以,所有内部类都可以使用外部类的私有成员。只要不违反 静态不能使用非静态的原则即可。
问:外部类能用成员内部类的私有成员吗?
答:可以。外部类与内部类是互相信任的关系。比父子类还要亲密。比喻:间谍侵入到组织内部,可以获取所有机密。
问:非静态内部类的非静态方法中,如果要区别是内部类的对象,还是外部类的对象,怎么做?
答:内部类的对象就是this,外部类的对象用 外部类名.this
4.7 总结
-
同一个类中,静态成员不能直接使用非静态成员。
-
跨类使用,
- A使用B的静态成员,B.静态成员
- A使用B的非静态成员,先创建B的对象b,b.非静态成员
-
外部类和内部类是互相信任的关系,可以互相使用对方的私有成员,但是不能违反上面两条
五、特殊类
5.1 新特性:记录类(了解)
Record类在JDK14、15预览特性,在JDK16中转正。
Record类是指常量类,它的实例变量都是final的,声明记录类的关键字是 record
记录类中会自动重写equals和hashCode,toString,get方法。当然,你也可以再次手动重写。
package com.test.special;
/*
(double a, double b, double c)既是有参构造的形参列表,也是Triangle类的3个final的实例变量。
*/
public record Triangle(double a, double b, double c) {
}
5.2 新特性:密封类(了解)
Java 15通过密封的类和接口来增强Java编程语言,这是新引入的预览功能并在Java 16中进行了二次预览,并在Java17最终确定下来。
回忆:
如果一个类加了final修饰,表示这个类不能被继承,没有子类。
如果一个类加了abstract修饰,表示这个类是抽象类,不能直接创建对象,需要创建它子类的对象,抽象类是用来被继承的。
现在:
密封类是用来限定某个类只能是被部分子类继承的写法。
- 密封类本身得有sealed关键字修饰,同时要用permits关键字来说明允许哪些子类可以继承它
- 密封类的子类必须是以下3种之一
- 继续是密封类sealed
- 恢复普通类 non-sealed
- 确定是断子绝孙类 final
package com.test.special;
/*
希望Graphic类只能被Circle,Rectangle类继承,不能被其他类继承
sealed:声明密封类的关键字
permits:声明该密封类只能允许哪些类继承它
Graphic类只能被Circle, Rectangle继承
*/
public sealed class Graphic permits Circle, Rectangle,Oval {//图形类
//关于类的成员,正常定义就可以
}
5.3 枚举类(掌握)
枚举类是一种对象是固定的有限的几个常量对象的类型。枚举类的对象不能在外面随便new,它的对象是提前new好的,外面只能用它new好的对象。
应用场景:星期、月份等类型,它们的对象就是固定new好的。
JDK5之前需要通过:
- 构造器私有化
- 在类的内部提前创建好几个常量对象供外面使用
JDK5之后,通过enum关键字来声明枚举类型,可以轻松实现枚举效果。
-
声明枚举的关键字是enum
-
此时所有构造器默认都是private,也只能是private
-
它的直接父类java.lang.Enum,也只能是Enum
-
它没有子类,因为它的构造器私有化,子类无法调用它的构造器
-
它会从Object和Enum类中继承一些方法
- String toString():默认返回常量对象名,当然我们可以继续重写。
- String name():返回常量对象名
- int ordinale():返回常量对象的下标
- static 枚举类型[] values()
-
建议枚举类的实例变量加final,因为枚举类的对象都是常量对象,所以它们的属性一般也不建议修改。
六、增强for循环
增强for循环是一个语法糖。它以一种更简洁的方式,来编写代码。对于遍历数组来说,本质上仍然是普通的for循环。
for(元素的类型 元素的临时名称 : 数组名或集合名){
System.out.println(元素的临时名称);
}
七、注解
注解是给代码加一些注释,这个注释不仅是给人看,还给编译器等程序来看。
例如:@Override
它的作用用于标记某个方法是重写父类或父接口的方法,编译器看到他之后,会对这个方法做格式检查,看是不是满足重写的要求。
例如:@Deprecated
用于标记某个方法或类,已过时。
例如:@SuppressWarnings("all") 抑制警告