• JavaSE学习之--抽象类和接口


     💕"没有眼泪我们就会迷路,彻底变成石头,我们的心会变成冰凌,吻会变成冰块。"💕

    作者:Mylvzi 

     文章主要内容:JavaSE学习之--抽象类,接口,内部类 

    目录

    一.抽象类

    1.抽象类的定义

    2.抽象类的语法规则--abstract关键字

    3.抽象类的注意事项

    1.抽象类无法实例化对象

    2.抽象类中可以存在普通成员变量,普通成员方法

    3.抽象方法不能被private修饰

    4.抽象方法不能被final和static修饰

    5.抽象类必须被继承,且子类必须重写抽象类所有的抽象方法(可以快捷键创建)

    6.抽象类可以存在构造方法,供子类初始化父类的成员变量(抽象类自己无法初始化)

    7.抽象类中不一定含有抽象方法,但抽象方法存在的类一定是抽象类!!! 

    二.接口(interface)

    1.接口的定义

    2.接口的语法

    1.接口关键字--》interface(替换原来的class),接口名一般以I开头(规范性)

    2.关于接口方法

    3.接口的实现--implements关键字

    3.接口的特性

    1.接口类型是一种引用类型,但是不能直接new接口对象(类似于抽象类)

    2.接口的方法默认都是被public abstract修饰的,使用其他修饰符会报错

    3.接口中的方法都不带有主体,只能有实现接口的类来实现

    4.重写接口的抽象方法时,不能使用默认权限,只能是public修饰

    5.接口中可以有变量,但变量都是被public static final修饰的

    6.接口中不能有构造方法和代码块

    7.接口编译完成的字节码文件的后缀格式也是.class

    8.在jdk8中:接口还可以包含default方法

    4.实现多接口

    5.接口之间的继承

    6.抽象类和接口的区别:

    三. Object类

    1.Object可以接受任意类型的对象

    2.Object本质上还是Java中的一个类,含有一些自带的方法

    1.toString方法--获取对象信息

    toString的源码

    2.equals方法--进行对象比较

     equals的源码:

    重写equals方法 :

    3.hashCode方法 

    hashCode的源码:

    重写hashCode方法: 

    四. 接口使用实例

      1.Comparable接口和Comparator接口

     2.Clonable 接口和深拷贝

    五.补充:内部类,外部类

    1.内部类:

    2.分类:

    1.实例内部类 

    2.静态内部类

    3.匿名内部类(通过接口实现)

    4.局部内部类

    补充: 


    一.抽象类

    1.抽象类的定义

            我们知道,对象是通过类来描绘的。但并不是所有的类都用来描绘对象,当你抽象出来的类所含有的信息不足以描绘出一个完整的对象时,这个类就叫做“抽象类”(所有的实现细节需要子类来自己完成)

        我们发现shape中draw方法无法被具体实现,需要在对应的子类中实现,我们把这种方法叫做“抽象方法”,抽象方法就是所有子类方法的“蓝图”,根据这个蓝图来创建不同的子类特有的方法!同时,含有抽象方法的类就是抽象类!

    2.抽象类的语法规则--abstract关键字

            抽象类往往是父类,子类继承抽象类实现子类特有的方法,创建抽象类是通过关键字abstract

    1. // abstract关键字创建抽象类
    2. abstract class Shape {
    3. // 创建抽象方法 不需要具体执行过程 是一个蓝图
    4. public abstract void draw();
    5. }

    3.抽象类的注意事项

    1.抽象类无法实例化对象

      抽象类包含的信息无法描绘一个完整的对象,所以无法通过抽象类来实例化对象!

            Shape shape = new Shape();
    

    2.抽象类中可以存在普通成员变量,普通成员方法

    1. abstract class Shape {
    2. // 普通成员变量
    3. int a;
    4. // 普通方法
    5. public void method() {
    6. System.out.println("普通方法!");
    7. }
    8. // 抽象方法--》不能含有函数体
    9. abstract void method2();
    10. }
    11. class Rect extends Shape {
    12. @Override
    13. public void method() {
    14. System.out.println("普通方法!");
    15. }
    16. @Override
    17. void method2() {
    18. System.out.println("抽象方法!");
    19. }
    20. }

    3.抽象方法不能被private修饰

      如果一个方法被private修饰,那他只能在此类中使用,也就是只能在抽象类中使用,而抽象类的方法就是用来被子类重写的,所以抽象类方法不能被private修饰

     

    4.抽象方法不能被final和static修饰

      如果一个方法被final修饰,则该方法无法被重写;如果被static修饰,那么此方法是属于类的,无法被重写 (要谨记,重写是子类对父类方法的重写,被static修饰的方法无法被继承或重写)

     

    5.抽象类必须被继承,且子类必须重写抽象类所有的抽象方法(可以快捷键创建)

    1. abstract class Shape {
    2. public abstract void draw();
    3. public abstract void trangle();
    4. }
    5. class Rect extends Shape {
    6. // 抽象方法必须被重写
    7. @Override
    8. public void draw() {
    9. System.out.println("矩形");
    10. }
    11. @Override
    12. public void trangle() {
    13. }
    14. }
    15. class Flower extends Shape {
    16. // trangle方法未被重写
    17. // 只重写一个抽象方法-->err
    18. // 所有的抽象方法必须被重写!
    19. @Override
    20. public void draw() {
    21. System.out.println("✿");
    22. }
    23. }

    注意到,这个报错的前一句是“Flower不是抽象的”,那是不是说明子类的也可以是抽象类呢?答案是可以的,Java中允许抽象子类,抽象子类将继续把抽象方法传递给他的子类,让他的子类来重写该抽象方法,形成了类的层级结构(但是不推荐将子类设置为抽象类,因为要重写多个方法) 

    1. abstract class Shape {
    2. public abstract void draw();
    3. public abstract void trangle();
    4. }
    5. abstract class Flower extends Shape {
    6. // 只重写一个抽象方法-->err
    7. // 所有的抽象方法必须被重写!
    8. @Override
    9. public void draw() {
    10. System.out.println("✿");
    11. }
    12. }
    13. class SmallFlower extends Flower {
    14. // 父类Flower的抽象类
    15. @Override
    16. public void trangle() {
    17. }
    18. // shape的抽象类
    19. @Override
    20. public void draw() {
    21. draw();
    22. }
    23. }

    6.抽象类可以存在构造方法,供子类初始化父类的成员变量(抽象类自己无法初始化)

    1. abstract class Shape {
    2. // 父类的成员变量
    3. int a;
    4. // 父类的构造方法
    5. public Shape(int a) {
    6. this.a = a;
    7. }
    8. // 抽象方法--》不能含有函数体
    9. abstract void method2();
    10. }
    11. class Rect extends Shape {
    12. int b;
    13. public Rect(int a,int b) {
    14. super(a);
    15. this.b = b
    16. }
    17. @Override
    18. void method2() {
    19. System.out.println(a);
    20. }
    21. }

    7.抽象类中不一定含有抽象方法,但抽象方法存在的类一定是抽象类!!! 

    抽象类存在的意义是什么?多一层编译检查!!!

    抽象类中抽象方法的存在强制了子类必须重写抽象方法,如果你想让所有的子类都包含某种方法,但具体的行为还要取决于具体的子类,就可以在抽象类(父类)中将此方法设置为抽象方法!


    普通类的方法可以被继承,重写,但普通类的方法是具体实现的,有可能通过子类误调用成父类的方法,而抽象类的抽象方法不是具体实现的,他是一个“模板”,不含有函数体,所有的子类都可以根据自身情况去重写该方法,且不会出现误调用的情况!

    二.接口(interface)

    1.接口的定义

      接口常见于我们的生活之中,比如最经典的是一直被诟病的苹果接口,众所周知,苹果接口只能给苹果手机充电,适用范围仅限于苹果设备,无法通过type-c接口充电,也就是说只有符合苹果规范的设备才能使用苹果接口!苹果接口是所有苹果设备的“共同规范”!

      计算机中的接口也是类似的,是一种“共同规范”,简而言之类似于“父类”,所有能使用此接口的类都符合接口设定的规范(也就是类含有接口中的方法!!!)

    2.接口的语法

    1. // 将class替换为interface
    2. public interface IShape {
    3. // 接口中的所有方法都默认是public abstract的
    4. public abstract void method1();
    5. void method2();
    6. // 接口中的成员变量都是默认被public static final修饰的
    7. }

    1.接口关键字--》interface(替换原来的class),接口名一般以I开头(规范性)

    2.关于接口方法

       接口中的所有方法都默认是public abstract的,为了代码的规范性和简洁性,推荐第二种写法(method2)

    3.接口的实现--implements关键字

        我们创建了接口,接下来就要使用接口,相当于你把苹果手机插入到苹果接口,接下来就要使用苹果手机了;在Java中,我们称之为类实现接口

      接口的实现是通过关键字--implements 

    1. public interface IShape {
    2. void drawMap();
    3. }
    4. // implements关键字代表此类实现该接口
    5. class Rect implements IShape {
    6. @Override
    7. public void drawMap() {
    8. System.out.println("矩形!!!");
    9. }
    10. }
    11. class Flower implements IShape {
    12. @Override
    13. public void drawMap() {
    14. System.out.println("❀❀❀");
    15. }
    16. }
    17. public class Testdemo {
    18. // 类似于类里面的向上转型!!!
    19. public static void drawMap2 (IShape ishape) {
    20. ishape.drawMap();
    21. }
    22. public static void main(String[] args) {
    23. drawMap2(new Rect());
    24. drawMap2(new Flower());
    25. }
    26. }

    注意事项:

    1.子类与父类是继承关系,类与接口是实现关系,接口和父类很相似,很多用法都是类似的

    2.接口中的方法都是抽象方法,实现接口的类必须重写接口中所有的方法!

    3.接口中不能存在普通方法 !!!

    4.一个例子

    代码实现:

    1. // USB接口
    2. public interface IUSB {
    3. void openDevice();
    4. void closeDevice();
    5. }
    6. // 鼠标类
    7. public class Mouse implements IUSB {
    8. // 实现USB接口就是重写USB接口中的所有方法
    9. @Override
    10. public void openDevice() {
    11. System.out.println("鼠标设备开启!");
    12. }
    13. @Override
    14. public void closeDevice() {
    15. System.out.println("鼠标设备关闭!");
    16. }
    17. public void clickMouse() {
    18. System.out.println("鼠标点击");
    19. }
    20. }
    21. // 键盘类
    22. public class KeyBoard implements IUSB {
    23. // 实现USB类
    24. @Override
    25. public void openDevice() {
    26. System.out.println("键盘设备开启!");
    27. }
    28. @Override
    29. public void closeDevice() {
    30. System.out.println("键盘设备关闭!");
    31. }
    32. public void inKeyBoard() {
    33. System.out.println("键盘输入!");
    34. }
    35. }
    36. // 计算机开机--通过usb使用相关设备--关机
    37. public class Computer {
    38. public void openComputer() {
    39. System.out.println("电脑开启!");
    40. }
    41. public void closeComputer() {
    42. System.out.println("电脑关闭!");
    43. }
    44. // 使用usb设备(类似于向上转型)
    45. public void useUSB(IUSB iusb) {
    46. // 注意这里开启,关闭设备是Mouse和KeyBoard类共有的,写在外面即可
    47. iusb.openDevice();
    48. // 利用关键字instanceof来判断引用的是哪个类
    49. if (iusb instanceof Mouse) {
    50. ((Mouse) iusb).clickMouse();
    51. //clickMouse是Mouse特有的方法,无法直接通过iusb实现
    52. /* Mouse mouse = (Mouse) iusb;
    53. mouse.clickMouse();*/
    54. }else if(iusb instanceof KeyBoard) {
    55. ((KeyBoard) iusb).inKeyBoard();
    56. }
    57. iusb.closeDevice();
    58. }
    59. }
    60. // 测试类
    61. public class Testdemo3 {
    62. public static void main(String[] args) {
    63. Computer computer = new Computer();
    64. computer.openComputer();
    65. // 设备使用
    66. computer.useUSB(new Mouse());
    67. computer.useUSB(new KeyBoard());
    68. computer.closeComputer();
    69. }
    70. }

    3.接口的特性

    1.接口类型是一种引用类型,但是不能直接new接口对象(类似于抽象类)

    1. interface IUSB {
    2. }
    3. public class Test1 {
    4. public static void main(String[] args) {
    5. IUSB iusb = new IUSB();
    6. // err
    7. }

    2.接口的方法默认都是被public abstract修饰的,使用其他修饰符会报错

    1. interface IUSB {
    2. void method1();
    3. public abstract void method2();
    4. private void method3();
    5. }

    3.接口中的方法都不带有主体,只能有实现接口的类来实现

    1. interface IUSB {
    2. void method1() {
    3. System.out.println("hehe");
    4. }
    5. }

    4.重写接口的抽象方法时,不能使用默认权限,只能是public修饰

      接口中的方法默认都是public权限的,实现接口的类中重写的方法的权限不能比Public低,所以只能是Public修饰的

    1. interface IUSB {
    2. // 默认是public abstract
    3. void method();
    4. }
    5. class Mouse implements IUSB {
    6. @Override
    7. // err
    8. // 接口中的方法都是public权限
    9. void method() {
    10. System.out.println("hehe");
    11. }
    12. }

    5.接口中可以有变量,但变量都是被public static final修饰的

       static:说明变量是属于类的,能直接通过类来访问

       final:证明变量无法被修改

    1. interface IUSB {
    2. int a = 10;
    3. }
    4. public static void main(String[] args) {
    5. System.out.println(IUSB.a);// 可以直接通过接口访问-->被static修饰
    6. IUSB.a = 20;// err无法被修改
    7. }

    6.接口中不能有构造方法和代码块

    1. public interface INTERFACE {
    2. // err
    3. {
    4. }
    5. // err
    6. static {
    7. }
    8. }

    7.接口编译完成的字节码文件的后缀格式也是.class

    8.在jdk8中:接口还可以包含default方法

       我们知道接口中的方法都是抽象方法,不含有主体,但在jdk8中引入了一个新特性-->default关键字,在接口中,如果方法被default修饰,则此方法可以含有主体,且在类中不必须被重写(相当于自动继承了)

    1. interface IUSB {
    2. void method1();
    3. default void method2() {
    4. System.out.println("This is a default method!");
    5. }
    6. }
    7. class Mouse implements IUSB {
    8. @Override
    9. public void method1() {
    10. System.out.println("hehe");
    11. }
    12. // method2不重写也不会报错
    13. // 相当于“自动继承”
    14. }
    15. public class Test1 {
    16. public static void main(String[] args) {
    17. Mouse mouse = new Mouse();
    18. mouse.method1();
    19. // 没有重写method2,也能调用该方法,证明该方法被“自动继承”了
    20. mouse.method2();
    21. }
    22. }

    4.实现多接口

      Java中类是单继承的,一个类无法继承多个类(不能有多个父亲),但可以同时实现多个接口!

    通过implements+','实现多个接口!!!

    下面通过类来表示一组动物

    1. class Animal {
    2. protected String name;
    3. public Animal(String name) {
    4. this.name = name;
    5. }
    6. }

    另外提供一些接口,分别表示 "会飞的", "会跑的", "会游泳的".

    1. interface IFlying {
    2. void fly();
    3. }
    4. interface ISwimming {
    5. void swim();
    6. }
    7. interface IRunning {
    8. void run();
    9. }

    猫类:跑

    1. // 猫类
    2. class Cat extends Animal implements IRunning {
    3. public Cat(String name) {
    4. super(name);
    5. }
    6. // ctrl+i快速调出接口抽象方法的重写
    7. @Override
    8. public void run() {
    9. System.out.println(this.name + "猫正在跑步");
    10. }
    11. }

    狗类:跑,游

    1. // 狗类 跑+游
    2. class Dog extends Animal implements IRunning,ISwimming {
    3. public Dog(String name) {
    4. super(name);
    5. }
    6. @Override
    7. public void swim() {
    8. System.out.println(this.name + "正在狗刨");
    9. }
    10. @Override
    11. public void run() {
    12. System.out.println(this.name + "狗正在跑步");
    13. }
    14. }

     鸭子类:跑,游,飞

    1. // 鸭子类 跑,飞,游
    2. class Duck extends Animal implements IRunning,ISwimming,IFlying {
    3. public Duck(String name) {
    4. super(name);
    5. }
    6. @Override
    7. public void fly() {
    8. System.out.println(this.name + "鸭子正在飞");
    9. }
    10. @Override
    11. public void swim() {
    12. System.out.println(this.name + "鸭子正在游泳");
    13. }
    14. @Override
    15. public void run() {
    16. System.out.println(this.name + "鸭子正在跑步");
    17. }
    18. }

     注意:一个类实现多个接口时,所有接口中的抽象方法都要被重写!!!

    1. public class Test1 {
    2. public static void main(String[] args) {
    3. Dog dog = new Dog("mimi");
    4. dog.run();
    5. dog.swim();
    6. System.out.println("================");
    7. Cat cat = new Cat("jiji");
    8. cat.run();
    9. System.out.println("================");
    10. Duck duck = new Duck("hehe");
    11. duck.fly();
    12. duck.run();
    13. duck.swim();
    14. }
    15. }

    上面代码展示了Java面向对象编程中最常见的用法:一个类继承一个父类,同时实现多个接口!!

    可以理解为接口时“部分共性”,但本质还是为了代码复用

    继承表达式是is-a语义,接口表达的含义是“具有xxx特性”

    • 猫是一种动物, 具有会跑的特性.
    • 狗也是一种动物, 既能跑, 也能游泳
    • 鸭子也是一种动物, 既能跑, 也能游, 还能飞

     这样做的好处是可以让程序员忘记类型,只关注实现特性,关注某个类是否具有该特性

    比如我们可以创建一个Robot类,他也可以实现IRunning接口,尽管他不是Animal类!

    1. class Robot implements IRunning {
    2. @Override
    3. public void run() {
    4. System.out.println("the robot is running!");
    5. }
    6. }

     创建一个方法调用接口中的方法(接口作为参数)

    1. public class Test1 {
    2. // 接口和类一样,可以作为参数类型,当对象传递时发生动态绑定
    3. // 谁实现了接口,谁就可以调用接口方法
    4. public static void Run(IRunning running) {
    5. running.run();
    6. }
    7. public static void main(String[] args) {
    8. // 匿名对象的直接调用
    9. Run(new Cat("jiji"));
    10. Run(new Dog("mimi"));
    11. Run(new Duck("hehe"));
    12. Run(new Robot());
    13. }

    5.接口之间的继承

      接口之间也存在继承关系,不同于类的是,一个接口可以继承多个接口

    1. interface IRunning {
    2. void run();
    3. }
    4. interface ISwimming {
    5. void swim();
    6. }
    7. // 两栖的动物, 既能跑, 也能游
    8. interface IAmphibious extends IRunning, ISwimming {
    9. }
    10. class Frog implements IAmphibious {
    11. ...
    12. }

          通过接口继承创建一个新的接口 IAmphibious 表示 "两栖的". 此时实现接口创建的 Frog 类, 就继续要实现 run 方 法, 也需要实现 swim 方法,同时继承两个接口!!!

    6.抽象类和接口的区别:

    1.成员变量:抽象类中可以含有普通成员变量,接口中的成员变量都是被public static final修饰的

    2.方法:抽象类中既可以有抽象方法也可以有普通方法,而接口中所有的方法都是public abstract修饰

    3.继承关系:一个类只能继承一个父类,但可以实现多个接口。但接口与接口之间只有继承关系 

     

    三. Object类

      Object类是Java中默认提供的一个类,他是所有类的父类,是类的“祖先”。为什么会有这么一个类呢?其实也很好理解,Java是一个面向对象编程的语言,它存在很多自定义的类,那程序员是如何写出这些自定义的类呢?本质上还是通过Object这个祖先类来开发的!!!

    1.Object可以接受任意类型的对象

    1. class Person{};
    2. class Stu{};
    3. public class Test1 {
    4. // 此处发生了向上转型
    5. public static void func(Object object) {
    6. System.out.println(object);
    7. }
    8. public static void main(String[] args) {
    9. func(new Person());
    10. func(new Stu());
    11. }
    12. }

    2.Object本质上还是Java中的一个类,含有一些自带的方法

       

    本文主要讲解equals,hashcode,toString方法

    1.toString方法--获取对象信息

        前面我们已经重写了很多toString方法,当时可能很不理解为什么toString方法要重写呢?他是来源于哪个类呢?现在可以解释这个问题了,toString方法是Object类自带的一个方法,所有的类都是Object的子类,所以要重写toString方法来实现我想获取的信息

    toString的源码

    2.equals方法--进行对象比较

      在Java中,==比较时

    如果左右两侧是基本数据类型(int等等),直接比较值的大小即可

    如果左右两侧是引用数据类型(比如对象),实际上比较的是引用类型的地址是否相同

    如果想比较对象中的内容,就要重写equals方法 

     equals的源码:

    1. public static void main(String[] args) {
    2. Person person1 = new Person();
    3. Person person2 = new Person();
    4. // 当两个引用类型比较时,实际上比较的是地址
    5. System.out.println(10 == 20);// false
    6. System.out.println(person1 == person2);// false
    7. System.out.println(person1.equals(person2));// false
    8. System.out.println("=======================");
    9. person2 = person1;
    10. System.out.println(person1.equals(person2));// true
    11. }

    两个对象的比较是判断对象的地址是否相同,也即是在内存中存储的位置是否相同 

    1. Person person1 = new Person("lisi",18);
    2. Person person2 = new Person("lisi",18);
    3. System.out.println(person1.toString());
    4. System.out.println(person2.toString());

    可以看出,两个对象的地址不同 

     

    重写equals方法 :

    假如我想使用Person类中的age来判断两个对象是否相同,此时就要重写equals方法

    1. class Person{
    2. int age;
    3. public Person(int age) {
    4. this.age = age;
    5. }
    6. @Override
    7. public boolean equals(Object object) {
    8. // 两个对象地址相同,直接返回true
    9. if (this == object) return true;
    10. // 比较的对象是null或两个对象的类型不同,直接返回false
    11. if (object == null || getClass() != object.getClass()) return false;
    12. // 此处进行向下转型
    13. Person person = (Person) object;
    14. return age == person.age;
    15. }
    16. @Override
    17. public int hashCode() {
    18. return Objects.hash(age);
    19. }
    20. }
    21. public class Test1 {
    22. public static void main(String[] args) {
    23. Person person1 = new Person(10);
    24. Person person2 = new Person(20);
    25. Person person3 = new Person(20);
    26. System.out.println(person1.equals(person2));// false
    27. System.out.println(person2.equals(person3));// true
    28. }

    注意:可以通过快捷键快速生成toString和hashcode的重写方法

    结论:通过对象中的内容进行比较时,一定要重写toString方法!

    3.hashCode方法 

      对象的hashCode值反应的是其在内存中的存储位置

    hashCode的源码:

    注:native代表此方法是由c/c++写的,无法查看真正的源码

    1. public static void main(String[] args) {
    2. // 创建两个内容完全相同的person对象
    3. Person person1 = new Person("lisi",18);
    4. Person person2 = new Person("lisi",18);
    5. // 验证他们在内存中是否位于同一地址
    6. System.out.println(person1.hashCode());
    7. System.out.println(person2.hashCode());
    8. // 结果显示并不位于同一块地址
    9. }

     

    重写hashCode方法: 

      同样的,我们也可以重写hashCode方法,实现只要内容完全相同,对象就处于内存中的同一块地址(快捷方法和toString一样)

    1. // 重写hashCode方法
    2. @Override
    3. public int hashCode() {
    4. return Objects.hash(name, age);
    5. }
    6. public static void main(String[] args) {
    7. // 创建两个内容完全相同的person对象
    8. Person person1 = new Person("lisi",18);
    9. Person person2 = new Person("lisi",18);
    10. // 验证他们在内存中是否位于同一地址
    11. System.out.println(person1.hashCode());
    12. System.out.println(person2.hashCode());
    13. }

     

     结论:

    1、hashCode方法用来确定对象在内存中存储的位置是否相同

    2、事实上hashCode() 在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的 散列码,进而确定该对象在散列表中的位置。

    总结:

      如果是自定义类型,记得一定要重写equals和hashCode方法,因为你的逻辑不是根据地址来判断类型是否相同,而是根据类型的属性来判断,所以要重写这两个方法 

    四. 接口使用实例

      1.Comparable接口和Comparator接口

    先设定一个学生对象,并创建一个学生数组

    1. class Student {
    2. String name;
    3. int age;
    4. public Student(String name, int age) {
    5. this.name = name;
    6. this.age = age;
    7. }
    8. }
    9. public class Test2 {
    10. public static void main(String[] args) {
    11. Student[] students = new Student[] {
    12. new Student("张三", 10),
    13. new Student("李四", 20),
    14. new Student("王五", 30),
    15. new Student("赵六", 40),
    16. };
    17. }
    18. }

       假如我们现在想通过年龄进行排序,能否直接利用Arrays.sort呢?

    Arrays.sort(students);// 可以直接这样排序吗?
    

      发现产生类型转换异常,原因在于之前使用Arrays.sort排序的数组是整形,可以直接通过比较数字的大小来进行排序的,而student是一个引用类型,无法直接进行排序,必须指定排序的依据,比如我现在想通过年龄进行排序,该怎么实现呢?通过Comparable接口!!!

    1. class Student implements Comparable{
    2. String name;
    3. int age;
    4. public Student(String name, int age) {
    5. this.name = name;
    6. this.age = age;
    7. }
    8. @Override
    9. public String toString() {
    10. return "Student{" +
    11. "name='" + name + '\'' +
    12. ", age=" + age +
    13. '}';
    14. }
    15. @Override
    16. // 重写compareTo
    17. public int compareTo(Object o) {
    18. Student s = (Student) o;
    19. /* if (this.age > s.age) {
    20. return 1;
    21. } else if (this.age == s.age) {
    22. return 0;
    23. }else {
    24. return -1;
    25. }*/
    26. return this.age-s.age;
    27. }
    28. }
    29. // 输出打印
    30. Arrays.sort(students);
    31. System.out.println(Arrays.toString(students));

      sort方法会自动调用compareTo方法,compareTo方法的参数是Object类型,要进行强制类型转换。通过重写Comparable接口中的compareTo方法实现根据类的属性进行比较的目的 

      如果数据类型是数字可以直接调用sort方法,如果数组的数据类型是对象,则要使每个对象具有“可比较性”,就是要让对应的类实现Comparable接口,并重写compareTo方法 ,你需要告诉编译器是通过类的哪项属性进行比较的

     但是我们发现这个方法也有一定的缺陷性,缺陷性在于我们重写compareTo方法时只能指定类的一个属性进行比较,比如在上述代码中的compareTo方法中,我们是通过age这个属性来比较的,但如果我们想通过名字比较呢?那不是要重写compareTo方法吗?可以看出,这样进行比较的方法可拓展性太差,对类的侵入性强,那有没有一种方法可以实现想通过什么比较就通过什么比较呢?答案是有的,即通过“比较器”来进行比较

    使用方法:构造一个比较类,实现Comparator接口,重写compare方法

    1. class Student{
    2. String name;
    3. int age;
    4. public Student(String name, int age) {
    5. this.name = name;
    6. this.age = age;
    7. }
    8. @Override
    9. public String toString() {
    10. return "Student{" +
    11. "name='" + name + '\'' +
    12. ", age=" + age +
    13. '}';
    14. }
    15. }
    16. // 表示比较的是Student对象(最好加上,在重写方法时会很方便)
    17. class AgeComparator implements Comparator {
    18. @Override
    19. public int compare(Student o1, Student o2) {
    20. return o1.age- o2.age;
    21. }
    22. }
    23. class NameComparator implements Comparator {
    24. @Override
    25. public int compare(Student o1, Student o2) {
    26. return o1.name.compareTo(o2.name);
    27. }
    28. }
    29. public class Test2 {
    30. public static void main(String[] args) {
    31. Student student1 = new Student("zhangsan",10);
    32. Student student2 = new Student("lisi",15);
    33. AgeComparator ageComparator = new AgeComparator();
    34. System.out.println(ageComparator.compare(student1, student2));
    35. NameComparator nameComparator = new NameComparator();
    36. System.out.println(nameComparator.compare(student1,student2));
    37. }

    解释一下:“return o1.name.compareTo(o2.name);”为什么通过name能直接调用compareTo方法?

    因为name是一个字符串类型,属于String类,而String类中有compareTo方法!!!

    注意:Comparator和Comparable是两个不同的接口,且用法也不同

    Comparator是为了构造比较器

    Comparable是使类具有可比较性

     为了进一步加深对接口的理解, 我们可以尝试自己实现一个 sort 方法来完成刚才的排序过程(使用冒泡排序

    1. // 能够排序的数组,要求数组元素必须具有可比较性,那把每个类型都设置为Comparable就能实现可比较性
    2. public static void bubble_sort(Comparable[] comparables) {
    3. for (int i = 0; i < comparables.length-1; i++) {
    4. for (int j = 0; j < comparables.length-1-i ; j++) {
    5. if(comparables[j].compareTo(comparables[j+1]) > 0) {
    6. // 不符合顺序就交换位置
    7. Comparable tmp = comparables[j];
    8. comparables[j] = comparables[j+1];
    9. comparables[j+1] = tmp;
    10. }
    11. }
    12. }
    13. }

     2.Clonable 接口和深拷贝

      Java中内置了很多有用的方法,clone就是其一,clone方法是Object类内置的一个方法,clone方法能够实现对象的拷贝,但要合法使用clone方法,需要先实现Clonable接口,否则会报错

    原型:

    代码实现: 

    1. class Money implements Cloneable {
    2. public double money = 19.9;
    3. }
    4. // 要想能clone,必须先实现Clonable接口
    5. class Stu implements Cloneable{
    6. int age;
    7. Money m;
    8. public Stu(int age) {
    9. this.age = age;
    10. m = new Money();
    11. }
    12. // 重写Object类中的clone方法
    13. // 访问权限是protected,只能跨包子类中使用
    14. @Override
    15. protected Object clone() throws CloneNotSupportedException {
    16. return super.clone();
    17. }
    18. }
    19. public class Testdemo1 {
    20. public static void main(String[] args) throws CloneNotSupportedException {
    21. Stu stu1 = new Stu(18);
    22. // 返回值是Object,所以必须进行强制类型转换
    23. Stu stu2 = (Stu) stu1.clone();
    24. System.out.println(stu1.m.money);// 19.9
    25. System.out.println(stu2.m.money);// 19.9
    26. System.out.println("================");
    27. }
    28. }

     注意:main后面必须添加:

    throws CloneNotSupportedException

    否则会报错

     

    鼠标移动到clone,同时按下:alt+enter,点击第一行即可自动添加

    现在我改变Stu2的m对象的money,再分别打印,看看会有什么结果

    1. // 返回值是Object,所以必须进行强制类型转换
    2. Stu stu2 = (Stu) stu1.clone();
    3. System.out.println(stu1.m.money);// 19.9
    4. System.out.println(stu2.m.money);// 19.9
    5. System.out.println("================");
    6. stu2.m.money = 20;
    7. System.out.println(stu1.m.money);
    8. System.out.println(stu2.m.money);

     可以发现不仅Stu2的Money改变了,Stu1的Money也改变了,可我们不是只改变了Stu2的money吗?原因在于Cloneable的拷贝是一种浅拷贝,所谓浅拷贝就是只拷贝当前对象(Stu),并不拷贝对象里面的对象(m) ,让我画图解释一下

     有浅拷贝就有深拷贝,深拷贝就是为了解决浅拷贝的缺陷存在的

    深拷贝:解决对象里面嵌套对象的拷贝

    拷贝方法:先克隆大对象,再克隆小对象(先暂存大对象)

    修改后的Stu类

    1. // 要想能clone,必须先实现Clonable接口
    2. class Stu implements Cloneable{
    3. int age;
    4. Money m;
    5. public Stu(int age) {
    6. this.age = age;
    7. m = new Money();
    8. }
    9. // 重写Object类中的clone方法
    10. // 访问权限是protected,只能跨包子类中使用
    11. @Override
    12. protected Object clone() throws CloneNotSupportedException {
    13. // 深拷贝:先克隆大对象,再克隆小对象
    14. Stu tmp = (Stu)super.clone();
    15. // 克隆小对象
    16. // 要克隆的是当前对象的里面的m对象,所以通过this表示当前对象调用
    17. // 将当前对象的m克隆到tmp的m之内,所以两边要对齐
    18. tmp.m = (Money)this.m.clone();
    19. return tmp;
    20. }
    21. }

    运行截图:

    深拷贝 浅拷贝看的是代码的实现过程,浅拷贝并不实现对象内的对象的拷贝,深拷贝实现对象内的对象的拷贝

    注意:能被拷贝的对象一定要实现clone接口!!! 

    五.补充:内部类,外部类

    1.内部类:

    定义在类里面或方法内部的类就叫做内部类

    2.分类:

       说内部类时一定要说清楚是什么内部类,对于内部类来说重点掌握静态内部类和匿名内部类

    1.实例内部类 

      在类中定义的不被static修饰的类就叫做实例内部类

    1. class OuterClass {
    2. int data1 = 1;
    3. int data2 = 2;
    4. // 实例内部类
    5. class InnerClass {
    6. int data3 = 3;
    7. int data4 = 4;
    8. // 实例内部类中的成员变量不能被static修饰,但可以是static final修饰
    9. // static int data5 = 5;
    10. static final int data10 = 10;
    11. public void method3() {
    12. System.out.println(data3);
    13. }
    14. public void method4() {
    15. // 访问外部类的成员变量,先实例化一个外部对象,通过外部对象访问成员变量
    16. OuterClass outerClass = new OuterClass();
    17. System.out.println(data4);
    18. System.out.println(data1);// 打印内部类的data1
    19. // System.out.println(data5);
    20. System.out.println("=================");
    21. System.out.println(OuterClass.this.data1);// 打印外部类的data1
    22. System.out.println(outerClass.data2);
    23. }
    24. }
    25. }
    26. public static void main(String[] args) {
    27. // 实例化一个实例内部类-->先实例化一个外部类对象,再实例化此外部类对象的内部类
    28. // 实例内部类依赖于外部类对象
    29. // 第一种写法(实例化出一个具体的外部类对象)
    30. OuterClass outerClass = new OuterClass();
    31. OuterClass.InnerClass innerClass = outerClass.new InnerClass();
    32. // 第二种写法(用了一个匿名的外部类对象)
    33. OuterClass.InnerClass innerClass2 = new OuterClass().new InnerClass();
    34. // OuterClass.InnerClass innerClass = new OuterClass.InnerClass();// err
    35. innerClass.method3();
    36. innerClass.method4();
    37. }

    总结:

    1.实例内部类依赖于外部类对象,必须先有外部类对象才能实例化内部类对象

    2.实例化一个内部类对象的方法有两种

    • 先实例化出一个具体的外部类对象,再实例化此外部类对象的内部类对象(注意语法格式)
    • 利用匿名外部类对象实例化内部类对象(很快速,但无具体的外部类对象) 

    3.实例内部类中的成员变量不能被static修饰,因为被static修饰的变量不依赖于对象,在类加载时就创建,而实例内部类的实例化又依赖于对象,两者矛盾。但如果被final修饰,则编译通过(此时已经是常量了) 

    4.当外部类成员变量和内部类成员变量相同时,会默认打印内部类的成员变量,Outerclass.this.访问外部类的成员变量 

    2.静态内部类

      在类中定义的被static修饰的类就叫做静态内部类

       我们都知道,类的成员变量如果被static修饰,那就说明这个成员变量是“类变量”,不依赖于对象。同样的,对于静态内部类的获取不依赖于外部类对象!

    1. class OuterClass {
    2. int data1 = 1;
    3. int data2 = 2;
    4. // 静态内部类
    5. static class InnerClass {
    6. int data3 = 3;
    7. int data4 = 4;
    8. static int data5 = 5;
    9. public void method3() {
    10. System.out.println(data3);
    11. }
    12. public void method4() {
    13. // 访问外部类的成员变量,先实例化一个外部对象,通过外部对象访问成员变量
    14. OuterClass outerClass = new OuterClass();
    15. System.out.println(data4);
    16. System.out.println(data5);
    17. System.out.println("=================");
    18. System.out.println(outerClass.data1);
    19. System.out.println(outerClass.data2);
    20. }
    21. }
    22. }
    23. public class Test1 {
    24. public static void main(String[] args) {
    25. // 如何实例化一个静态内部类对象呢?
    26. // 一定是先有外部类,再有内部类,也就是说一定要指明内部类的归属
    27. OuterClass.InnerClass innerClass = new OuterClass.InnerClass();
    28. innerClass.method3();
    29. innerClass.method4();
    30. }
    31. }

    总结:
    1.静态内部类是定义在类内部的,被static修饰的类 

    2.实例化一个静态内部类:外部类名.内部类名--》一定是先有外部类,再有内部类,也就是说一定要指明内部类的归属

    3.在内部类中想要访问外部类的成员变量--》先实例化一个外部类对象,通过外部类对象访问成员变量

    3.匿名内部类(通过接口实现)

    1. // 匿名内部类
    2. interface A {
    3. void methodA();
    4. }
    5. public class Test1 {
    6. public static void main(String[] args) {
    7. int a = 10;
    8. a = 20;// err如果修改,在匿名内部类中就无法打印
    9. final int b = 20;
    10. new A(){// 以下代码相当于一个类实现了A接口,并重写了A的方法,但是这个类没有名称,所以叫做匿名内部类()通过接口实现
    11. @Override
    12. public void methodA() {
    13. // System.out.println(a);
    14. System.out.println(b);
    15. System.out.println("hehe!!!");
    16. }
    17. }.methodA();
    18. }
    19. }

    总结:

    1.在匿名内部类中只能打印被final修饰数据(常量) 或只被初始化未被修改过的数据(如果修改,就无法访问),所以建议在匿名内部类中访问的数据都设置为final修饰的

    2.匿名内部类是通过接口实现的,调用方法有两种

    • 直接在花括号外.方法名
    • 创建一个对象,通过对象调用

    4.局部内部类

      在方法中定义的类就是局部内部类

    1. public class OutClass {
    2. int a = 10;
    3. public void method(){
    4. int b = 10;
    5. // 局部内部类:定义在方法体内部
    6. // 不能被public、static等访问限定符修饰
    7. class InnerClass{
    8. public void methodInnerClass(){
    9. System.out.println(a);
    10. System.out.println(b);
    11. }
    12. }
    13. // 只能在该方法体内部使用,其他位置都不能用
    14. InnerClass innerClass = new InnerClass();
    15. innerClass.methodInnerClass();
    16. }
    17. public static void main(String[] args) {
    18. // OutClass.InnerClass innerClass = null; 编译失败
    19. }
    20. }

    总结:
    1.局部内部类只能在定义的方法之中使用,子其他位置均无法使用

    2.局部内部类非常少见,了解即可

    补充: 

    一个类对应一个字节码文件

    实例,静态内部类和匿名内部类的字节码文件名称不同,

    实例,静态内部类的字节码文件名称:外部类名$内部类名

    匿名内部类的字节码文件名称:外部类类名$数字

    总结:
      本文主要讲述了抽象类,接口的定义,使用,以及两者的区别。抽象类和接口都是面向对象编程的重要语法,要好好理解他们的作用和基础的语法,会大大提高代码的效率!最后还介绍了一种特殊的类--》内部类 ,重点掌握静态内部类和匿名内部类

  • 相关阅读:
    打不动的蓝桥杯
    ThingsBoard IoT Gateway 实战(三)- 使用 MQTT Connector 转接灯
    KNN算法学习笔记+应用实例
    2024.6.5 刷题总结
    day4作业
    Linux——网络编程总结性学习
    用 Wireshark 让你看见 TCP 到底是什么样!
    Android|集成 slf4j + logback 作为日志框架
    OpenStack裸金属ironic组件web-console界面定制
    测试报告。
  • 原文地址:https://blog.csdn.net/Mylvzi/article/details/133280768