java核心技术卷1 第五章:继承

学习重要的是出,而不是入,此前一直埋头向前学,忽视了复习的重要性。写一个博客作为自己的学习笔记,也可作为以后查漏补缺的资料,温故而知新。

类,超类和子类

  1. 一个继承另一个类,父类也称为超类,基类。"超类"中的超来自于集合理论,指的是父类,与之后的super关键字对应
  2. java中,类的继承默认为public继承(只有公共继承),与c++不同。
  3. 子类public继承父类,所以继承到的字段和方法的访问权限都不变,和c++同,子类中无法直接访问父类的private成员(继承到了,但是无法访问)
  4. 子类中可以overrride父类方法,覆写后直接调用使用的就是自己定义的方法。如在一个覆写方法中直接调用父类同名方法会循环调用直到爆栈,应该加super
  5. super只是一个指示调用父类方法的关键字,而不是任何的对象引用
  6. 子类可以覆写,增加字段和方法,但不会删除继承到的字段和方法

子类的构造函数

因为子类继承了父类的字段,实例化对象是需要初始化这些字段,但是子类可能不能直接访问这些字段(private)
解决方法

构造函数中的第一句使用super(),内部填写参数,对应父类的一个构造函数,这样会调用父类的构造函数来初始化父类的那部分字段。
如果不显示调用super(),那么会默认调用父类无参构造函数,这时候父类必须可以提供一个无参的构造函数

this和super

作为第一句出现:

this:在一个类的构造函数内的第一句调用,可以用来调用其他构造函数来初始化部分的字段

super:子类构造函数第一句调用,初始化父类字段

作为一个调用指示词

this:指示当前的对象,可以用来获得当前对象的字段和方法
super:获得父类的字段和方法

多态

指的是一个类型的对象可以引用(指向,指示)多种不同的类型(子类),这种特性称为多态

而在调用方法时可以根据实际指向的对象类型自动选择对应对象的方法,称为动态绑定


c++中的多态需要用virtual关键词,而java中默认的对象赋值行为就是多态的

如果父类对象中的方法是final的,则不允许Override,从而实现动态绑定

继承层次结构

继承同一个父类的不同子类彼此是独立的,并无直接关系

java不支持多继承

c++支持一个类同时继承多个父类,java不支持

继承原则

只有两个类满足 is-a ,才适合用继承。一个类是另一个类,只是更加特殊。

替换原则:is-a还指出:因为子类就是父类,所以任何需要父类的地方,都可以用子类替代,父类可以直接引用(指向)子类

父类可以通过引用子类,来调用对应的方法,实现不同的行为。

其中:

  1. 父类只能调用父类中定义的方法,因为这实际上是一个父类指针,只能看见父类的成员

  2. 只能把子类赋值给父类,而不能把父类赋值给子类。只能多->少,而不能少->多

  3. 进行方法调用时,会从所有的候选方法中选出最佳的调用。(参数匹配->参数类型兼容...),若多个可能匹配则报错

函数签名

函数签名(函数名和参数),返回值不是前面的一部分,只要函数名和参数一致,就判断是相同方法

一个方法和另一个方法相同,是说函数名和参数一致。

当子类中定义了一个和父类函数名和参数相同的方法,就代表覆写了父类的方法,而返回值不一样一模一样,只要现在的返回值类型和原本父类的类型兼容,也算是合法的方法重写。兼容指的是可以给原本类型赋值的类型,比如子类到父类的兼容

静态绑定和动态绑定

静态绑定:可以直接确定调用的是哪个对象的方法:private,final和static修饰的方法

private,子类中无法访问,所以调用一定是父类的

final:不允许重写的方法

static:属于一个类的方法,因此通过类和对象调用一定是对应类中的方法

动态绑定:调用一个方法是,调用实际指向的哪个对象的方法,如果子类中重写了,那么调用子类的方法逻辑,数据使用子类中定义的字段(相当于子类在调用),如果没有重写,那么调用父类的方法

如果不是private,final,static修饰的方法,都将采用动态绑定的方式,实际调用的方法是实际引用的哪个对象类型的方法


warning

重写父类的方法是,子类中的方法可见性不可以低于父类,只能宽松可见性,而不能设置更严格的可见性

阻止继承

使用final关键字修饰class(放在class前):代表不允许派生子类

使用final关键字修饰方法,代表子类不允许重写这个方法。

final修饰的类的所有方法自动称为final方法。但是不包括字段,因为字段加上final代表是const,不可变,和final用了同一个关键词,但是代表不同的含义

将类或方法声明为final的原因:不希望子类来捣乱,这样一个类型的对象引用的一定是对应类型的对象,而不会发生多态,调用不一样行为的方法


强制类型转换

可以在继承层次结构内进行强制类型转换(对象引用变量的强制转换

  • 可以子类自然的转成父类,比如直接赋值使用,但是父类转子类需要强制转换,且不一定成功

  • 转换失败会抛出ClassCastException异常,不捕获程序就结束了

  • 进行强制类型转换的原因:要使用原本对象的完整功能,如把一个被父类指向的子类还原成子类,从而可以调用子类中新增加的方法

  • instanceof:用于判断一个对象是否是另一个对象,是否is-a关系?前面的引用变量应该属于后面的类是,继承或进行了接口实现。比如一个父类对象引用指向了一个子类,如Person person = new Student();,那么 person instanceof Student 是true,因为person对象虽然是父类,但是实际上指向子类,所以其是子类的instance,如果其指向父类,判断就会返回falsePerson person = new Person();,person instanceof Student。

父类转子类需要强制类型转换,如果其是父类引用到子类,那么将这个对象引用转为子类可以成功。因为子类赋值给父类,父类实际上指向的是子类对象,但是由于类型,它的能力被限制了,只能访问到父类定义的那部分,所以可以强制转换成功,恢复全部的能力。

但是如果父类本身指向的是父类,而直接转换为子类,就会报异常,是一个ClassCastException异常,因为父类实际上没有定义子类那部分成员,所以转换失败

instanceof:

  • 子类对象 instanceof 父类,返回true,子类是父类的实例,is-a
  • 父类对象 instanceof 子类,看父类对象的引用对象,如果引用对应的子类,返回true,可以强制转换成对应子类恢复全部能力,如果不是,返回false

受保护访问

protected:一个包下可以访问,或是其他包的子类可以访问。

应当慎用保护,或是干脆不用。

比如使用protected字段,其他类继承后使用,可以直接访问到类的具体细节,他们的实现就会依赖这个字段,如果你决定重写,那么其他人的代码可能会变得不可用

相比之下,protected修饰方法显得更有意义一些,只让那些很 熟悉父类实现的子类可以访问到这个方法,而其他的类不可以。(但是同一个包下的类还是可以访问到,所以protected修饰符意义并不大,少用)


Object:所有类的父类

object是所有类的父类

java中,只有数值,字符和布尔类型不是对象,是属于基本类型,primitive type,而其他所有的都是对象,比如数组,无论是基本类型的数组还是对象数组,都是Object类的子类

c++与之类似的概念是,所有指针都可以转换为void*指针,而没有一个所有类的基类

equals方法

Object的equals方法,比较两个对象的地址是否相同,很简单,因此很多类都需要重写这个方法来实现比较的逻辑

其中:

  • 一个对象为null,则equals返回fase
  • 两个对象都是null,则返回true
  • 比较对象,需要先调用父类的equals方法,父类的字段相等之后再继续比较子类的字段

其中,java语言规范要求实现的equals方法有以下性质

  1. 自反性:任意非null对象x,x.equals(x)应该返回true
  2. 对称性:x.equals(y)和y.equals(x)相同
  3. 传递性:x.equals(y),y.equals(z),那么x.equals(z)也为真
  4. 非null引用x.equals(null),返回false

当属于同一个类的对象比较,这很自然。但当两个比较的对象不是同一个类时,会变得很麻烦

  • 使用getClass,instanceof来判断是否相等,可能会造成对称性被违反,比如父类和子类,交换次序执行getClass,instanceof返回结果是不同的
  • 因此,对于不同类的对象进行equals比较时,很麻烦,很多类库API实现都没有做到对称且正确

Override注解

告诉编译器,这里的方法是在重写父类的方法,如果编译器发现其实是在定义新方法而不是重写, 就报错

hashcode方法

如果x.equals(y)为true,那么这两个对象调用hashcode方法返回的因该是相同的int值。

hashcode:散列码,如果对象不同,那么hashcode值也基本不会相同

hashcode返回一个int,可以是负数

默认Object类实现的hashcode基于地址算出,如果对象地址相同,hashcode就相同

String类重写了hashcode方法,因为equals根据字符串内容判断,如果字符串内容相同那么相同,这时候的hashcod也相同,而不是根据地址判断

Object类提供了一个hash方法,可以接收多个参数,返回这些参数hashcode的组合,得到一个组合所有参数字段后的hashcode

基本数据类型的hashcode:通过其对应的包装类:Double,Integer等类的hashcode方法得到

toString方法

类方法中应该定义一个toString方法,打印对象的状态信息,帮助使用者理解对象状态

当调用println打印对象,就会调用这个类的toString方法,当对象通过+与一个字符串拼接时,java编译器也会自动调用toString方法获得对象的字符串描述

如果不重写, 默认会有一个toString方法,这里本机测试是:类名@hashcode

应该可以通过getClass.getName()方法获得对象的类名

许多类库中定义的类都有toString()方法,帮助我们获得和对象状态有关的信息

强烈建议每个类定义一个toString,方便自己和使用者

ArrayList,泛型数组列表

一个可变大小的数组,其定义为泛型。

声明:

  1. 其中左边的<>中填入对象类型,不能是基本数值类型,基本数值类型需要用对应的包装类
  2. 右边的<>默认是Object,不用填入内容,填入也会被忽略
  3. ()中可以填入容量,这里的容量是可以随着增加删除动态改变,但是容量不够时需要开辟空间,然后复制内容,比较耗时,因此尽量选择合适的容量
  4. 如ensureCapacity方法,可以指定一个容量,让创建的数组列表可以包含有这么多的容量,避免后面容量不足的耗时操作
  5. trimToSize方法,可以削减容量,由垃圾回收器收回多余的存储容量,而只保存现有的元素存储空间

'''

public class Hello {
    public static void main(String[] args) {
        ArrayList<Person> people=new ArrayList<>();
        people.add(new Student());
        people.add(new Employee());
        for(var person:people)
            System.out.println(person.getName());

    }
}

'''

使用:

  1. 定义一个容量
  2. 通过add增加元素,通过set更改已增加元素的值,通过get获得指定索引的值
  3. 容量!=size,size是实际包含的元素数量,set,get也只能改变和获得已经add进来的元素,而不能是其他的元素

注意:

ArrayList类似于C++的vector,但是没有重载[],只能通过get(index)访问元素

其他问题

  • 插入和删除的效率低,如用add(index,e)和remove(index)方法,内部的元素都要移动来保持顺序存储,类似于顺序线性表的性质
  • 支持for each的循环访问
  • set(index,e)替换内容后返回原本那个位置的内容

对象包装器与自动装箱

基本类型的数据需要转换为对象是,用到的是其对应的对象包装器类。

有Integer,Long,Double,Float,Short,Byte,Character,Boolean。对应不同的基本数据类型,其中,前面的六个继承了Number公共类。

这些包装器类的性质

  • 不可变类。也就是其中对应的基本类型值确定后就不可以改变,final
  • final类,不允许派生
  • ArrayList<int>不允许,因为类型需要是对象,所以要使用包装器类

自动装箱与拆箱

使用基本类型和使用包装类很一致,需要包装类的地方直接填入基本类型也可以,如ArrayList<Integer>.add方法,可以直接填入ArrayList.add(7),这会自动改写为ArrayList.add(Integer.valueOf(7)),而把一个Integer对象赋值给一个int,会自动拆箱,变成int型。类似于调用了Integer.intValue().

大多数情况下,基本可以认为,基本类型和他们的包装类是一样的

一些例外:

  • 如ArrayList<>中填类型,一定要是包装类
  • 作比较时,包装器类作为对象比较的是地址,需要调用equals方法才能正确比较数值

一些注意点:

  • 构造包装器对象:调用valueOf方法,来赋值,而不是用new Integer(7)这样的,这种嗲用构造器的方法从version9就被废弃了
  • 装箱和拆箱是编译器的工作,当其产生字节码后,相关的操作已经被完成了,虚拟机只负责执行这些字节码
  • 一些很有用的方法被放在了包装类中,比如将字符串转为int,调用Integer的parseInt方法就可以,传入一个String可以转为int
  • 包装器的一些方法:
    1. Integer的static方法:toString(int i,int radix),可以传入两个参数,来将一个数值转为对应进制的字符串,比如传入8,就可以把这个8传唤为2进制的1000字符串
    2. parseInt:可传入两个参数,表示要用多少进制来解析这个字符串到十进制int值
    3. valueOf:初始化一个Integer,可以是int值,也可以传入String,传入String可以一个参数或者两个参数,两个参数就可以再指定一个进制,表示用多少进制来解析这个字符串到int

参数个数可变的方法

可以提供可变的参数数量给方法

使用:类型之后加上... ,表示可接受多个此类型的参数,需作为最后一个参数.

public void hello(int i,String... strings){}

这里传入多个String之后,会保存在strings[]数组中,用strings[]数组访问传入的数量可变的参数

  • 因为传入的参数会被放入一个数组中,因此使用...可变参数和使用数组作为参数是等价的
  • 传入可变个参数,也可以直接传入一个数组对象,这样进入方法后也可以访问到数组的各个元素,等价于传入了可变个参数
  • 如果一个方法的最后一个参数是数组,那么其就可以直接替换为...可变参数,使用完全等价
  • 如Main函数,就可以声明参数为String... args

#补充

当需要接受对象参数时,如Object[],如果传入基本数据类型,则会自动装箱为对应的包装类

只有实际的数据可以被包装,类型不会自动转换,如ArraryList<int>不会自动变为ArraryList<Integer>,而add加入参数时会把一个7自动包装为Integer.valueOf(7)


抽象类

将一些公共的字段和方法提取,作为父类,其中可以有一些方法不做实现,作为占位符,等待子类继承后实现。这种父类中不做具体实现的方法称为抽象方法,abstract方法,为了程序清晰,有抽象方法的类必须声明为抽象类,abstract class

  • 抽象类中可以包含具体的字段和方法
  • 子类中可以实现抽象方法。如果不实现,那么子类也需要声明为抽象的
  • 不含有抽象方法,也可以声明类为抽象类
  • 抽象类不能实例化,不可以创建抽象类的对象,即不可以new,构造出一个真正的对象
  • 可以创建抽象类的引用变量,指向具体实现的子类,从而实现多态
  • 一个经典用法是创建父类数组,其中的值均为子类的对象,体现子类is-a父类关系

密封类(mark,使用场景暂不清楚,项目场景下理解)

java15预览特性,java17最终确定

密封类可以控制哪些类可以继承它:保证自己控制继承体系,保持完整功能,而不被外部随意继承打乱

public abstract sealed class JSONValue
	permits JSONArrary,HSOONNumber
{
...
}

这里sealed关键字表示密封类,permits表示允许继承的子类

  • 允许的子类必须可见,可访问,不可是一个类的内部私有类,不允许是一个包内的包内访问类
  • 允许的公共public子类,必须和密封类一个包下
  • 可以不加permits语句,但是所有可以继承的子类都要和密封类一个文件下

允许继承的子类:必须是final,sealed或是non-sealed

  • final代表子类不允许继承
  • sealed表示允许指定的类继承
  • non-sealed表示允许所有的继承

non-sealed:第一个带连字符的java关键字

  • 连字符可能带来一些风险,导致现有代码无法编译,比如定义一个int non和int sealed变量,可以计算non-sealed,相减得到一个结果,而再jdk17版本中这个表达式会被解析为一个关键字
  • 连字符可能会成为java关键字的一个趋势

反射

暂时略,写java开发工具使用多,关注应用程序开发的使用较少

继承的设计技巧

  1. 公共的字段和方法提取作为父类,而不子类中多次重复定义

  2. 不适用protected保护字段

    • 意义不大,因为并不能提供太多保护机制
    • 子类是随意派生的,任何人任何地方都可以派生一个子类,访问protected字段,破坏封装性
    • 同一个包下,即使不是子类也可以访问protected字段,破坏封装性
  3. 不滥用继承,只有是is-a关系再使用继承,如果不是,将其作为平级的独立类而不是作为继承关系

  4. 利用多态实现基于类型的不同动作,而不是用if-else判断类型后指向不同的动作

posted @ 2024-03-17 11:13  shueykkk  阅读(35)  评论(0编辑  收藏  举报