java全端课4--面向对象(下)

40 阅读19分钟

一、接口

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") 抑制警告