5 面向对象

5.1 类和对象

可以把类当成一种自定义的引用类型

5.1.1 定义类

类是某一批对象的抽象,对象是一个具体存在的实体

  • 定义类:

     [修饰符] class 类名{
     // 成员变量
     // 成员方法
     // 构造器
         // 初始化块
         // 内部类
     }
  • 类里面的各成员的顺序没有影响,可以相互调用
  1. 成员变量

     [修饰符] 类型 成员变量名 (=初始值);
    • 小驼峰命名
  2. 成员方法

     [修饰符] 返回值类型 方法名(形参列表){
         // 方法体
     }
    • 如果声明了返回值类型,方法体中必须有一个有效的return语句
    • 小驼峰命名,建议以动词开头
    • static修饰的成员表明它属于这个类本身,而不属于对象
  3. 构造器

    •  [修饰符] 构造器名(形参列表){
       // 
       }
    • 构造器是一个特殊的方法
    • 构造器名必须和类名相同
    • 实际上构造器是有返回值的,当使用 new来调用构造器时,返回该类的对象
    • 如果程序员没有为一个类编写构造器,则系统会提供一个默认的构造器

      5.1.2 对象的产生和使用

      创建对象的根本途径是构造器

    
    #### 5.1.3 对象、引用和指针
    
    **对于** `Person p = new Person();`这句代码,引用变量p里存放的仅仅是一个引用(地址),指向实际的对象,当访问p引用变量的成员变量和成员方法时,实际上访问的是p引用的对象的成员变量和成员方法。如下图:
    
    
    <iframe src="https://viewer.diagrams.net/?tags=%7B%7D&highlight=0000ff&edit=_blank&layers=1&nav=1&title=%E6%9C%AA%E5%91%BD%E5%90%8D%E7%BB%98%E5%9B%BE.drawio#R5ZnLcpswFIafhmUz3I2XieM0i3omHbfTZNVRQAG1AlFZxNCnrzASNxkbt7GdJtlEOrogfb84Ogdr1izOP1KQRgsSQKyZepBr1rVmmoZuefxfaSkqi2tPK0NIUSA6NYYl%2Bg3lSGHNUABXnY6MEMxQ2jX6JEmgzzo2QClZd7s9Edx9agpCqBiWPsCq9RsKWFRZPXPS2G8hCiP5ZMMV%2B4uB7Cx2sopAQNYtkzXXrBklhFWlOJ9BXMKTXKpxNwOt9cIoTNiYATBb2F8Tc3L3%2Febzl4VrObeL2QermuUZ4ExsWCyWFZIAn4XD5pWrdYQYXKbAL1vWXG9ui1iMec3gRTEVpAzmg2s06p3zIwNJDBkteBcxwHIFLHFapqK6btBbtrBFLeymJ4xAyB3WUzdEeEFAOQCQtx9QSEmWDm5eHEzwKLvrh0IxJl0ohqlSmW6DYh8LiqNA0XODK%2BO4E0%2FBw%2BkkASyn01%2FoDA0LpTIUzEx9HLOjIXNHvGhJcFl6LF5LSAK7YLoUYY7Yfav80Cpf5%2B1KISrV02CgeLsxWPkySUZ9uO9IqPhbuJ0tuKWNQgwYeu6ubZsG4gl3BPFV129I%2FVIVAye%2FWr4Y1faRciLZkQEaQqZ03Eheb%2BvvT8Hkfz4F51J32hNXP1Tc3jwn0tpQr46c%2F53AP%2B6%2BTuzeHVtfLy01Df2UvtGYqvfJq8NUR6RnwyT30MKUKpj4hlmXxYpR8hPOCCa08SlPCOOeCWAUJrzqc0CQ269KfIhHwpeiIUZBgIfiwK48L8DfsLr8LUvl727Bbx0Nv6ngT0AM364Ctt53FM6ZFVCzlTJ7ezcCmMa5BXDUcIXHEktRJZRFJCQJwPPG2sPS9PlESCr0%2BQEZK0TuDzJGuurJMEeWN2HOhbMz0OGEaVEPKisP7ZZm0KYmR%2FkZfd6stLlDdsZKwjQuODb1KuDYe8DV83BYAMXDR1C0OqRlhLMajq%2F6SbfMsIfiqH5%2FeSybk1Wt4EWjKXNbAuXi8kV%2F5IWwLGhzV7vUNc%2BTLfxJTeObdROG19PPO%2FdNuS3NUbRyNlq570sr%2B9VpNeIDVysl9TFYrZC%2FKyvtONF%2F8t36hTtxO%2F77wnIme3z4pnYHKeJ8SsHH5r1jfblM6%2FY68wFffpqc2e4lzbXPPjRpduzeREZvoiNn0aaaG3Iv8Ssrv5pf3TfFt%2BsynP7n3%2BOF4bza%2FB5RKdj8qmPN%2FwA%3D" allow-top-navigation="false" allow-forms="false" allowfullscreen="true" allow-popups="false" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>
    
    
    **如果堆内存中的对象没有任何引用指向它,那么程序将无法访问该对象,这个对象就变成了垃圾,Java的垃圾回收器将回收该对象,释放该对象所占的内存**
    
    #### 5.1.4 对象的this引用
    
    **Java提供的** `this`关键字可以理解为:是指向对象本身的一个引用
    
    `this`的最大作用就是=让类中的一个方法访问该类的另一个方法或成员变量=,大部分时候,一个方法访问该类中定义的其他方法或成员变量时,加不加 `this`的效果是一样的
  4. 如果在 static修饰的方法中使用 this关键字,则 this无法指向合适的对象,所以 static修饰的方法中不能使用 this,在静态方法中访问另一个普通方法,只能新建一个对象
  5. 如果方法中有局部变量和成员变量同名,又需要在这个方法中使用这个被覆盖的成员变量,就要使用 this
  6. this在构造器中代表正在被构造的对象

    5.2 方法详解

    5.2.1 方法的所属性

    Java里的方法不能独立存在,所有的方法都必须定义在类里,在逻辑上要么属于类,要么属于对象,执行方法时,必须使用类或对象作为调用者。

    5.2.2 方法的参数传递机制

    Java中方法的传参机制只有一种:=值传递=。所谓值传递就是将实际参数值的复制品传入方法内,而参数本身不会有任何影响。

    5.2.3 形参个数可变的方法

    从JDK1.5之后,如果在定义方法时,在最后一个形参的类型后,加 ...,则表明该形参可以接收多个参数值。

     public void method(int a, String... b)
  7. 一个方法中只能有一个可变形参
  8. 位置只能放在形参列表的最后
  9. 调用时,可以传入多个参数,也可以传数组

    5.2.4 递归方法

    5.2.5 方法重载

    如果=同一个类=中包含了两个或两个以上=方法名相同=但=形参列表不同=的方法,被称为方法重载。

    形参列表不同体现在:类型、个数、顺序

    5.3 成员变量和局部变量

    5.3.1 成员变量和局部变量

  10. 类变量从类的准备阶段开始存在,直到系统销毁这个类;实例变量从该实例被创建开始存在
  11. 类变量也可以让实例来访问,如果通过一个实例修改了类变量的值,会导致该类的其他实例访问这个变量时得到的是被修改之后的值
  12. 成员变量无需显式初始化,系统会在类的准备阶段或创建对象时默认初始化
  13. 局部变量除了形参外都需要显式初始化才能被访问
  14. 形参无需显式初始化,形参的初始化在调用方法时由系统完成:当调用方法时,系统会在该方法栈区为所有的形参分配内存空间,并将实参的值赋给对应的形参

    5.3.2 成员变量的初始化和内存中的运行机制

    略。注意类成员变量和实例成员变量的区别

    5.3.3 局部变量的初始化和内存中的运行机制

    局部变量定义后,必须经过显式初始化才能够使用,系统不会为局部变量默认初始化。定义局部变量后,系统并未为这个变量分配内存空间,直到这个变量被赋初值时,系统才会为这个局部变量分配内存,并将初始值保存在这块内存中

    局部变量不属实任何类或实例。它总是被保存在其所在的方法的栈内存中。栈内存的变量无需垃圾回收,随着方法或代码块的结束而结束。

    5.3.4 变量的使用规则

    成员变量的使用情形:

  15. 用于描述某个类或某个对象的固有信息
  16. 用于保存该类或实例运行时的状态信息
  17. 用于在该类的多个方法间共享

    使用局部变量也应该尽量缩小局部变量的作用范围,局部变量的作用范围越小,它在内存中停留的时间越短,程序运行的性能就越好

    5.4 隐藏和封装

    Java程序推荐将成员变量进行封装

    5.4.1 理解封装

    封装(Encapsulation),是面向对象的三大特征之一。它是指将对象的状态信息=隐藏=在对象内部,不允许外部程序直接访问对象内部消息,而是通过该类所=提供的方法=来实现对内部信息的操作和访问

    封装的好处:

  18. 隐藏类的实现细节
  19. 让使用者只能通过预定的方法来访问数据,从而可以在方法里加入控制逻辑,限制不合理访问
  20. 可进行数据检查,从而保证对象信息的完整性
  21. 便于修改,提高代码的可维护性

    为了实现良好的封装,应该:

  22. 将对象的成员变量和实现细节隐藏起来,不允许外部直接访问
  23. 把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作

    5.4.2 使用访问控制符

    Java提供了4种访问级别,由小到大分别是:private < default < protected < public

    privatedefaultprotectedpublic
    同一个类中
    同一个包中
    子类中
    全局范围中
  24. 访问控制符用于控制一个类的成员是否可以被其他类访问,对于局部变量而言,其作用域就是它所在的方法,比不可能被其他类访问,所以无需访问控制符
  25. 对于外部类而言,它不在任何类的内部,它的上一级是包,要么在同一个包中要么不在同一个包中,所以它只有public或者默认

    使用访问控制符的原则:

  26. 类里的绝大部分成员变量应该用 private修饰,只有一些 static修饰的、类似于全局变量的成员变量,才考虑用 public修饰
    有些方法只用于辅助实现该类的其他方法,被称作工具方法,也应该用 private修饰
  27. 如果某个类主要用作其他类的父类,该类里包含的方法仅希望被其子类重写,而不想被外界直接调用,应该使用 protected修饰方法
  28. 希望暴露出来给其他类自由调用的方法应该用 public修饰(类的构造器允许其他地方创建该类的实例,所以用public修饰)

    😎5.4.3 package、import、import static

    各个公司提供的各种类,不可避免出现重名问题,怎么解决呢?

    Oracle允许在类名前加一个前缀来限定这个类,Java引入了包(package)机制,提供了类的多层命名空间,用于解决类的命名冲突、类文件管理等问题

    Java允许将一组功能相关的类放在用一个package下,组成逻辑上的类库单元,位于包中的每个类的完整类名都应该是包名和类名的组合(全限定类名)

     package 包名;// 应放在第一个非注释行

    5.4.4 Java的常用包

  29. java.lang:核心类。System Math String Thread
  30. java.util:工具类、集合。Arrays List Set
  31. java.sql:Java进行JDBC编程相关的类和接口
  32. java.io:输入/输出相关类和接口
  33. java.net:网络编程相关类和接口
  34. java.awt:抽象窗口工具集,主要用于构建GUI程序
  35. java.text:格式化相关的类
  36. java.swing:Swing图形用户界面相关类和接口

    5.5 深入构造器

    构造器是一种特殊的方法,用于创建实例时执行初始化。即使使用工厂模式、反射等方式创建实例,其本质依然是依赖于构造器,所以Java类必须有构造器。

    5.5.1 使用构造器执行初始化

    当创建一个对象时,系统会为这个对象的实例变量进行默认初始化(把所有的基本类型的成员变量初始化为 0,把所有的引用类型的成员变量初始化为 null),如果想要改变这种默认初始化,就可以通过构造器实现

    当调用构造器时,系统会先为该对象分配内存空间,并执行默认初始化,对象已经产生。这个对象还不能被外部程序访问,只能在该构造器中通过 this来引用

    如果程序员没有为一个类编写构造器,则系统会提供一个默认的无参构造器,这个构造器执行体为空;一旦程序员提供了自定义的构造器,系统就不再提供默认的构造器

    构造器主要在其他类中用于创建类的实例,通常构造器的权限设成 public,从而允许任何位置可以创建该类的对象。除非在极端的情况下,比如,设置成 private,阻止其他类创建该类的实例

    5.5.2 构造器重载

    如果一个类里提供了多个形参列表不同的构造器,就形成了构造器的重载

    构造器可以通过 this来调用另一个构造器,且=必须放在第一行=

     public class Person {
         private String name;
         private int age;
     
         public Person(String name) {
             this.name = name;
         }
     
         public Person(String name, int age) {
             this(name);// here
             this.age = age;
         }
     }

    5.6 类的继承

    继承(Inheritance),是面向对象的三大特征之一,是实现软件复用的重要手段。Java的继承和C++不一样,是单继承。

    5.6.1 继承的特点

    子类 extends 父类/基类/超类

    子类是一种特殊的父类(苹果是一种水果),父类的包含的范围比子类包含的范围要大

    extends作为继承的关键字,意思是‘扩展’,子类是对父类的扩展,是一种特殊的父类,可以获得父类全部的成员变量成员方法内部类(包括内部接口和枚举),但是不能获得父类的的构造器初始化块

    Java的继承是单继承,就是只有一个直接父类,但是可以有多个间接父类。如果定义一个类的时候没有显式指定直接父类,则这个类默认继承java.lang.Object类。因此,java.lang.Object是所有类的父类,所有Java对象都可以调用java.lang.Object中的实例方法

    5.6.2 重写父类的方法

    子类包含与父类同名方法叫方法重写(override),也被称为方法覆盖

    方法重写的原则:两同两小一大

  37. 两同:方法名相同,形参列表相同
  38. 两小:子类方法的=返回值类型=比父类的更小或相等;子类方法=抛出的异常类=比父类的更小或相等
  39. 一大:子类方法的=访问权限=比父类的更大或相等
  40. 要么都是类方法,要么都是对象方法
  41. 方法被定义为 final 不能被重写
  42. 如果父类的方法具有 private权限,那么该方法对子类就是隐藏的,就不能被重写,就算子类中定义了与这个方法同名同参同返回的方法,依然不是重写而是一个新方法

    5.6.3 super限定

    super用于限定该对象调用从父类继承来的实例变量或方法

  43. 当子类重写了父类的方法后,如果需要在子类中调用父类被覆盖的方法,可以使用 super关键字
    this一样,super也不能出现在 static修饰的方法中,因为静态方法是属于类的,调用者是一个类而不是对象,因而 super就失去了意义
  44. 如果子类定义了和父类重名的实例变量,则会发生子类实例变量隐藏父类实例变量的情形(这时候会为子类对象分配两块内存,一块用于存储自身的实例变量,另一块用于存储继承来的实例变量)
  45. 如果被覆盖的是类变量,在子类中可以通过父类名作为调用者来访问被覆盖的类变量

    如果在某个方法中访问名为a的变量,但没有显式指定调用者,查找a的顺序为:当前方法是否有名为a的局部变量 -> 当前类中是否有名为a的成员变量 -> 上溯当前类的所有父类是否有名为a的成员变量 -> 编译错误

    当系统创建一个对象时,不仅会为当前类定义的实例变量分配内存,还会为从父类继承来的所有实例变量分配内存

    5.6.4 调用父类的构造器

    子类不会继承父类的构造器,但是子类的构造器可以调用父类构造器

    class Base{
     public double size;
     public String name;
     public Base(double size, String name) {
         this.size = size;
         this.name = name;
     }
    }
    public class Sub extends Base{
     public String color;
    
     public Sub(double size, String name, String color) {
         super(size, name);
         this.color = color;
     } 
    }

    super调用父类构造器必须出现在子类构造器的第一行,所以super调用和this调用不能同时出现

    不管是否使用super调用来执行父类构造器的初始化代码,子类构造器总会调用父类构造器一次:

  46. 使用super显式调用父类构造器
  47. 使用this显式调用本类中重载的构造器,执行本类中另一个构造器时会先调用父类构造器
  48. 系统会执行子类构造器之前,隐式调用父类的无参构造器

    所以,创建任何对象,最先执行的总是java.lang.Object类的构造器

    5.7 多态

    Java的引用变量有编译时类型(由声明该变量时使用的类型决定)和运行时类型(由实际赋给该变量的对象决定),如果编译时类型和运行时类型不一致,就有可能出现多态。

    5.7.1 多态性

    因为子类是一种特殊的父类,所以Java允许把一个子类对象直接赋给一个父类类型的引用变量,无需类型转换,这被称为向上转型(upcasting)

    相同类型的变量,调用同一个=方法=时呈现出多种不同的行为特征,这就是多态。与方法不同的是,实例变量不具多态性

    5.7.2 引用变量的强制类型转换

    引用变量只能调用它编译时类型含有的方法,而不能调用它运行时类型含有的方法,如果想要调用运行时类型的方法,必须把它强转为运行时类型

    引用类型间的类型转换只能在具有继承关系的两个类型之间进行,考虑到进行强制类型转换有可能发生异常,因此在强转之前应先通过 instanceof来判断一下

    5.7 .3 instanceof运算符

    instanceof的前面通常是一个引用类型的变量,后面是一个类或接口,它用于判断前面的对象是都是后面的类或者其子类、实现类的实例

    使用 instanceof时,如果前后两者没有父子关系,就会引起编译错误

    ❌5.8 继承与组合

    5.9 初始化块

    和构造器作用类似,用于对象的初始化操作

    5.9.1 使用初始化块

    (static) {
     // 
    }

    实例初始化块在创建对象时隐式执行,而且先于构造器执行;类初始化块在类初始化阶段自动执行

    多个初始化块按顺序执行

    当创建对象时,先为所有的实例变量分配内存(前提时该类已经被加载过),默认初始化。再然后执行初始化,顺序是:先执行实例初始化块或声明变量是的初始值,再执行构造器

    5.9.2 实例初始化块和构造器

    实例初始化块和构造器的作用相似,且优先于构造器执行

    与构造器不同的是,实例初始化块是一段固定执行的代码,它不能接收参数,对所有对象执行的初始化操作都完全相同。

    实际上实例初始化块是一个假象,代码经过编译后,实例出适合块中的代码会被“还原”到每个构造器的最前面

    与构造器相同的是,创建对象时,不仅会执行当前类的实例初始化块,而且会一直上溯到java.lang.Object类

    5.9.3 类初始化块

    使用 static修饰的代码块

    类初始化块在类初始化阶段执行,并且也会一直上溯到java.lang.Object类

    6 面向对象

    ❌6.1 包装类

    Java有8种基本数据类型,不具备“对象”的特性

    Java为这8种基本数据类型分别定义了相应的引用类型,称之为包装类

    基本数据类型包装类
    byteByte
    shortShort
    intInteger
    longLong
    charCharacter
    floatFloat
    doubleDouble
    booleanBoolean

    在JDK1.5之前,基本类型和包装类之间的转换需要借助包装类的方法,这有些繁琐。所以JDK1.5提供了自动拆装箱机制。

  49. 自动装箱(Autoboxing):把一个基本数据类型的值直接赋值给=对应的=包装类变量。例如:Integer a = 1;
  50. 自动拆箱(AutoUnboxing):把包装类对象直接赋值给基本类型变量

    包装类还可以实现基本类型和字符串之间的转换

  51. 字符串转基本类型

    1. 包装类提供的 parseXxx(String s)静态方法
    2. 包装类提供的 valueOf(String s)静态方法
  52. 基本类型转字符串

    1. String.valueOf()
    2. 基本类型 + “”

    虽然包装类是引用数据类型,但包装类的实例可以与数值类型的值进行比较

    6.2 处理对象

    6.2.1 打印对象和toString方法

    当使用 System.out.println()方法打印对象时,实际上输出的是这个对象的 toString()方法的返回值

    toString()是Object类中的一个实例方法,是一个“自我描述”的方法,通常用于这样的功能:输出该对象具有的状态信息

    直接打印一个对象得到的结果是这样的:类名+@+hashCode,这样并不能实现“自我描述”功能,所以需要重写Object的 toString()方法

    例如:

    public String toString(){
     return "Apple[color=" + color + ",weight=" + weighr + "]";
    }

    6.2.2 ==和equals方法

  53. ==

    • 判断基本类型变量的数值是否相等
    • 判断引用类型变量指向的是否是同一个对象
  54. equals()
    是Object类的一个实例方法,源码如下:

    public boolean equals(Object obj) {
     return (this == obj);
    }

    因此,单纯的 equals()并没有特殊的意义,如果希望自定义相等的标准,就要重写 equals()方法,比如:String重写了 equals()方法,它判断两个字符串相等的规则是字符序列相同

    • 通常重写 equals()方法应遵循以下:
    1. 自反性
    2. 对称性
    3. 传递性
    4. 一致性
    5. null

    6.3 类成员

    static

    6.3.1 理解类成员

    类成员属于整个类,而不属于单个对象

    当系统第一次使用该类时,系统会为该类变量分配内存空间并执行初始化,类变量开始生效,直到该类被卸载,该类变量分配的内存才会被垃圾回收机制回收

    类变量既可以通过类来访问,也可以通过该类的对象来访问。但通过对象访问类变量时,实际上并不是访问该对象拥有的变量,因为系统创建对象时不会再为类变量分配内存执行初始化。

    同一个类的所有对象访问类变量时,实际上访问的都是该类所持有的变量

    当使用对象访问类成员时,实际上是委托该类来访问类成员,因此即使某个对象指向null,也依旧可以访问类成员;而一个null对象访问实例成员,则会出现 NullPointException

    对于 static而言,有一点非常重要:类成员不能访问实例成员。因为完全有可能出现类成员已经初始化完成,实例成员还没有初始化的情况

    ❌6.3.2 单例(Singleton)类

    6.4 final修饰符

    final表示不可变

    6.4.1 final成员变量

    final修饰的成员变量,一旦有了初始值,就不能被重新赋值

    final修饰的成员变量必须由程序员显式地指定初始值。要么在声明变量的时候指定,要么在初始化块,要么在构造器中指定

    final修饰的成员变量在显式初始化之前不能直接访问,但是可以通过方法访问,这应该是一个缺陷,建议开发的时候避免在final成员变量显式初始化前访问它

    6.4.2 final局部变量

    final修饰的局部变量,无论是在定义的时候就指定值还是先定义再赋值都可以,但只能一次

    final修饰的形参不能被赋值,它是由调用方法时传参来完成初始化的

    6.4.3 final修饰基本类型变量和引用类型变量的区别

    final修饰基本类型变量时,不能对基本类型变量重新赋值,也就是说基本类型变量不能改变了;final修饰引用类型变量时,只保证这个引用地址不变,即一直引用同一个对象,但是对象本身可以发生改变

    理解:final只保证存的东西不能被改变

    6.4.4 可执行“宏替换”的final变量

    final变量只要满足以下三个条件,这个final变量就相当于一个直接量。这个final变量的本质就是一个“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值

  55. 使用 final修饰
  56. 定义变量时指定了初始值
  57. 该初始值可以在编译时就确定

    总结:定义final变量时,指定可以在编译时就确定的初始值

    例如:final int a = 1;对于这句代码而言,变量a就是不存在的,会直接用1替代

    6.4.5 final方法

    final修饰的方法不可被重写(不是重载),如果不希望子类重写父类的某个方法,可以使用final修饰

    对于父类的private方法,之前说过,子类无法访问,即使子类中有同名同参同返回的方法,那也是新方法,因此是否使用final修饰并没有什么影响

    6.4.6 final类

    final修饰的类不可以有子类,如果不希望某个类可以被继承,可以使用final修饰这个类

    ❌6.4.7 不可变类

    ❌6.4.8 缓存实例的不可变类

    6.5 抽象类

    类中定义方法用来描述类的行为方式,但在某种情况下,某个父类只知道子类应该包含什么样的方法,但不知道子类如何实现该方法。

    (既然不知道子类中方法的具体实现,那父类中能不能直接不管呢?试想以下场景:父类引用指向子类实例,那么这个引用变量就无法调用这个方法,除非强转)

    使用抽象方法就可以满足以上需求

    6.5.1 抽象方法和抽象类

    抽象方法是只有方法签名,没有方法实现的方法。有抽象方法的类必须被定义成抽象类,但是抽象类里可以没有抽象方法

    抽象方法和抽象类的规则如下:

  58. 抽象方法和抽象类必须使用 abstract修饰
  59. 抽象类不能被实例化,抽象类的构造器主要是用于被其子类调用

    定义抽象方法:在普通方法上增加 abstract,把普通方法的方法体替换成 ;

  60. 示例:public abstract void test();

    定义抽象类:在普通类上增加 abstract

    利用抽象类和抽象方法的优势,可以更好地发挥多态的优势,使得程序更加灵活

    抽象类只能被继承,抽象方法必须要重写,所以 abstractfinal永远不可能同时使用

    静态方法不能被定义成抽象方法(调用一个没有方法体的方法会引起错误),所以 abstractstatic不能同时修饰一个方法

    抽象方法必须被子类重写才有意义,否则这个方法永远不会有方法体,所以抽象方法不能是 private权限,所以 abstractprivate不能同时使用

    6.5.2 抽象类的作用

    抽象类是从多个具体类中抽象出来的父类,体现的是一种模板模式的设计,以这个抽象类作为子类的模板,避免子类设计的随意性,子类在抽象类的基础上进行扩展,但总体上会大致保留抽象类的行为方式

    6.6 Java9改进的接口

    6.6.1 接口的概念

    接口定义了某一批类需要遵守的规范,接口不关心这些类的内部状态数据和方法实现细节,只规定这些类里必须提供某些方法。可见,接口是从多个相似类中抽象出来的规范,接口不提供任何实现,体现的是规范和实现分离的设计哲学

    6.6.4 Java9中接口的定义

    (public) interface 接口名 extends 父接口1, 父接口2 ... {
     // 成员变量(只能说静态常量)
     // 内部类、内部接口、内部枚举
     // 抽象方法(无方法体)
     // 默认方法、类方法、私有方法(有方法体)
    }

    一些说明

  61. 接口名应采用大驼峰命名方式,通常使用形容词
  62. 一个接口可以有多个直接父类接口,接口只能继承接口不能继承类
  63. 接口成员的可以省略访问控制修饰符,如果指定只能用 public
  64. 接口不能有构造器和初始化块
  65. 接口中定义的成员变量,系统会自动加上 public static final修饰,且只能在定义时指定初始值
  66. 接口中定义的内部类、内部接口、内部枚举,系统会自动加上 public static修饰
  67. 接口中定义的普通方法,系统会自动加上 public abstract修饰,普通方法不能有方法体
  68. 接口中定义的默认方法出现在Java 8,必须使用 default修饰;无论是否指定,默认方法总是 public;不能使用 static修饰,所以不能使用接口调用,只能使用接口的实现类的实例来调用。
    默认方法就是有方法体的实例方法
  69. 接口中定义的类方法出现在Java 8,必须使用 static修饰;无论是否指定,类方法总是 public;可以直接使用接口来调用
  70. 接口中定义的私有方法出现在Java 9,必须使用 private修饰;Java 8允许带方法体的默认方法和类方法,势必有可能出现相同的实现逻辑代码,如果抽取出工具方法,工具方法应该被隐藏,就出现了私有方法

    6.3.3 接口的继承

    接口支持多继承,获得父接口里的所有成员变量和抽象方法

    6.6.4 使用接口

    接口不能用于创建实例,但接口可以用于声明引用类型变量,这个引用类型变量必须引用到实现类的对象

    接口的主要作用就是被实现类实现,实现使用 implements关键字,实现一个接口可以获得接口里定义的成员变量、方法(包括抽象方和默认方法)

    一个类可以实现多个接口,这个类必须重写这些接口里所定义的全部抽象方法;否则,该类将保留从父接口实现来的抽象方法,这时候这个类就得定义成抽象类了

    实现接口方法时,必须使用 public修饰(子类方法不能缩小父类方法的访问权限)

    6.6.5 接口和抽象类

    接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外界提供哪些服务;对于接口的调用者而言,交口规定了调用者可以调用哪些服务以及如何调用这些服务。当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序间使用接口时,接口是多个程序间的通信标准

    抽象类体现的是一种模板式设计。抽象类作为多个子类的共同父类,可以当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能(已经实现的方法),但这个产品不能当成最终产品,必须进一步完善,这种完善可能有不同的形式

    使用上的异同:

  71. 相同点

    1. 都不能被实例化,用于被其他类继承或实现
    2. 都可以包含抽象方法,继承抽象类或实现接口的普通类必须实现这些抽象方法
  72. 不同点

    1. 接口的成员变量必须是静态的;抽象类的成员变量可以不是静态的
    2. 接口不能有有方法体的普通方法而抽象类可以有
    3. 接口没有构造器而抽象类可以有
    4. 接口没有初始化块而抽象类可以有
    5. 接口是多继承,抽象类是单继承

    ❌6.6.6 面向接口编程

    6.7 内部类

    定义在其他类内部的类叫做内部类,包含内部类的类就被称为外部类

    内部类的好处:

  73. 提供了更好的封装。内部类隐藏在外部类内部,不允许同一个包的其他类访问(比如Person 和 Leg)
  74. 内部类可以直接访问外部类的私有数据,但是外部类不能访问内部类的细节
  75. 匿名内部类适用于仅需要使用一次的类

    内部类相比于外部类可以多使用以下三种修饰符:private`protected`static

    6.7.1 非静态内部类

    定义一个内部类仅需要把一个类放在另一个类内部即可,这个“内部”可以是任意位置,甚至是方法中(方法中的内部类叫局部类)

    大部分时候,内部类都是被作为成员内部类定义而不是局部内部类。既然是成员,就可以使用访问控制符

    使用 static修饰的叫做静态内部类,反之就是非静态内部类

    成员内部类的class文件总是这种形式:OuterClass$InnerClass.class

    在非静态内部类的对象里,保存了一个它所寄生的外部类对象的引用

    当在非静态内部类的方法中使用某个变量时,查找的顺序是:该法内的局部变量 -> 内部类的成员变量 -> 外部类的成员变量 -> 编译错误:提示找不到该变量

    如果外部类成员变量、内部类成员变量、内部类方法局部变量重名,可以使用 this外部类类名.this区分

    非静态内部类可以直接访问外部类的成员,但是反过来不行(创建外部类对象的时候,非静态内部类的对象还不存在)。如果外部类想要访问非静态内部类的实例成员,必须显式创建非静态内部类的的对象

    根据静态成员不能访问非静态成员的规则,外部类的静态成员(静态方法、静态代码块)不能访问非静态内部类

    非静态内部类不能拥有静态成员

    6.7.2 静态内部类

    使用 static修饰的内部类叫静态内部类,也叫类内部类。静态内部类属于外部类本身,不属于外部类的某个对象

    静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员

    静态内部类是外部类的一个静态成员,所以外部类的所有方法、初始化块中都可以使用静态内部类来定义变量、创建对象

    外部类不能直接访问静态内部类的成员,但可以使用静态内部类的类名来访问静态内部类的成员

    ❌6.7.3 使用内部类

    6.7.4 局部内部类

    定义在方法里的内部类叫局部内部类,局部内部类仅在方法中有效

    如果需要用局部内部类定义变量、创建实例或派生子类,都只能在局部内部类所在的方法内部进行

    局部内部类的class文件总是这种形式:OuterClass$NInnerClass.class$后面多了一个 N,因为同一个类里不能出现同名的成员内部类,但是可以出现同名的局部内部类(不在同一个方法中),使用数字做区分

    实际开发中很少使用局部内部类

    6.7.5 匿名内部类

    匿名内部类适用于那种只需要使用一次的类

    定义方法:

    new 接口() | 父类(){
     //
    }

    定义匿名内部类时无须 class关键字,而是在定义匿名内部类时直接生成该匿名内部类的对象

    注意:

  76. 匿名内部类不能是抽象类(因为抽象类不能被创建对象,而系统创建匿名内部类时,会立即创建匿名内部类的对象)
  77. 匿名内部类不能定义构造器(匿名内部类没有类名,就无法定义构造器)

    匿名内部类不能是抽象类,所以匿名内部类必须实现接口或抽象父类里的全部抽象方法,如果有需要,可以重写抽象父类种的普通方法

    当通过实现接口来创建匿名内部类时,匿名内部类不能显式地定义构造器,因此匿名内部类只有一个隐式的无参构造器;当通过继承抽象父类来创建匿名内部类时,匿名内部类将拥有和父类相似的构造器,相似指的是有相同的形参列表

    如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了 final修饰

    6.8 Java11增强的Lambda表达式

    Lambda表达式是Java8的新特性,支持将代码块作为方法参数,允许使用更简洁的代码来创建只有一个抽象方法的接口的实例

    6.8.1 Lambda表达式入门

    Lambd表达式就相当于一个匿名方法,当使用Lambda表达式代替匿名内部类创建对象时,Lambda表达式的代码块就会替代匿名内部类中实现抽象方法的方法体

    由三部分组成

  78. 形参列表。允许忽略形参类型;只有一个形参的时候,圆括号都可以省略
  79. ->。
  80. 代码块。代码块只有一条语句的话可以省略 {};Lambda表达式需要返回值,只有一条语句的话可以省略 return

    Lambda表达式整体就像一个“任意类型”的对象

    6.8.2 Lambda表达式与函数式接口

    Lambda表达式的类型被称为“目标类型(target type)”,Lambda表达式的目标类型必须是“函数式接口(functional interface)”,函数式接口是只包含=一个抽象方法=的接口(可以有多个默认方法、类方法)

    如果采用匿名内部类创建函数式接口的实例,只需要实现一个抽象方法,这时可以采用Lambda表达式来创建对象,创建出来的对象的目标类型就是这个函数式接口

    Java 8提供了 @FunctionalInterface注解,用于编译器检查是否是函数式接口

    6.8.3 在Lambda表达式中使用var

    6.8.4 方法引用与构造器引用

    如果Lambda表达式中只有一条语句,还可以在代码块中使用方法引用和构造器引用

    ::表示

  81. 引用类方法
    形式:类名::类方法
    说明:函数式接口中被实现方法的全部参数传给该类方法作为参数
    对应的Lambda表达式:(a,b,...) -> 类名.类方法(a,b,...)
    示例:

    @FunctionalInterface
    interface Converter{
        Integer convert(String from);
    }
    
    // 使用Lambda表达式创建对象
    Converter converter = from -> Integer.valueOf(from);
    // 可以写成
    Converter converter = Integer::valueOf;
    
    Integer number = converter.convert("100");// 100
  82. 引用特定对象的实例方法
    形式:特定对象::实例方法
    说明:函数式接口中被实现方法的全部参数传给该类方法作为参数
    对应的Lambda表达式:(a,b,...) -> 特定对象.实例方法(a,b,...)
    示例:

    @FunctionalInterface
    interface Converter{
        Integer convert(String from);
    }
    
    // 使用Lambda表达式创建对象
    Converter converter = from -> "fkit.org".indexOf(from);
    // 可以写成
    Converter converter = "fkit.org"::indexOf;
    
3. **引用某类对象的实例方法**
**形式:**`类名::实例方法`
**说明:函数式接口中被实现方法的第一个参数作为调用者,后面的参数传给该实例方法作为参数**
**对应的Lambda表达式:**`(a,b,...) -> a.实例方法(b,...)`
4. **引用构造器**
**形式:**`类名::new`
**说明:函数式接口中被实现方法的全部参数传给该类方法作为参数**
**对应的Lambda表达式:**`(a,b,...) -> new 类名(a,b,...)`

#### ❌6.8.5 Lambda表达式与匿名内部类的联系与区别

#### ❌6.8.6 使用Lambda表达式调用Arrays的类方法

### 6.9 枚举类

**实例个数有限且固定的类被称为枚举类,比如季节**

#### 6.9.1 手动实现枚举类

**在早期的代码中,可能会使用简单的静态常量来表示枚举**

**也可采用定义类的方式来实现**

#### 6.9.2 枚举类入门

### ❌6.10 对象与垃圾回收

### ❌6.11 修饰符的适用范围

### ❌6.12 多版本JAR包

## 反射
最后修改:2022 年 12 月 17 日
如果觉得我的文章对你有用,请随意赞赏