Java基础复习(类的继承,接口的本质,抽象类,异常,包装类,泛型)

类的继承

为什么要针对接口编程?继承不好用吗?难道就因为java是单继承,所以才搞出接口来实现所谓的多继承?
首先,继承是把双刃剑。

继承的好处:

  1. 代码复用,公共属性和方法可以丢到基类中去,子类只需要关注子类特有的就行。
  2. 通过基类可以方便统一处理不同的子类,如上转型对象。

继承的痛点

  1. 破坏了封装,封装可谓面相对象三大特性之一,是面相对象编程基本思维。那怎么破坏封装了呢?子类如果要重写(扩展)父类方法,要知道基类中方法实现的细节,要弄清楚父类中方法之间的依赖,比如子类要重写父类中的A方法,而父类中的A方法调用了这个父类中的B方法,那么就要再看一看B方法中的实现细节。否则会可能有错误,举例先忽略。同样的,父类中如果要修改方法,那也要考虑到子类。这样就破坏了封装性。如图:
    Java基础复习(类的继承,接口的本质,抽象类,异常,包装类,泛型)

Java基础复习(类的继承,接口的本质,抽象类,异常,包装类,泛型)

2.破坏了is-a的关系,要知道继承关系就是is-a关系。那破坏从何说起?举个例子,比如说有个基类是鸟类,里面有个方法叫fly(),子类要重写这个方法来实现特定的功能。顾名思义,基类中是希望子类是有 飞 这个能力的,但如果子类有个是企鹅,它又不能飞,只会游和走,那该怎么办?就在企鹅的fly()中写游泳吗?这样虽然没问题,但显然是破坏了is-a关系。父类中拥有的属性和行为,子类中不一定都适用,但子类还是可以重写方法,实现和父类中预期不一致的行为。比如,在用基类统一处理的时候 调用fly()是希望出来 飞 这个动作,结果却有一个是 游泳 。。。举例可能不恰当,但明白意思就OK!

好了,解决办法是什么呢?

  1. 用final,来规定不能被继承的类或者方法。
  2. 用组合来代替继承。可以代码复用,但却不能通过基类统一处理各种子类。
  3. 使用接口
    终于说到接口了!

很多情况下我们只关注实现的功能,而不需要在意是什么类型实现了这个功能。比如加热,电磁炉可以,煤气灶也行,农村的灶也行。所以可以把功能单独抽出来,做成接口,比如 加热接口。
使用接口可以降低代码耦合,也就是挽救继承破坏了封装性的痛点,也同时解决破坏is-a关系(因为不用继承了,自然也没is-a了)。使用接口虽然可以实现统一管理,但接口中没有实现方法,所以不能解决代码复用问题。
因此,最终解决办法
——接口+组合。

接口的本质

在什么情况下我们会用到接口?在一些场景下,我们不需要关心是什么类型,只需要关心实现的功能。比如加热,微波炉,电磁炉,煤气灶都可以加热,但我只要加热这个功能(能力)就OK。
Java8和9对接口进行了加强。
Java8中允许在接口中定义两类新的方法:1.静态方法 2.默认方法,这两种方法都必须为public。在这之前接口中的方法都是public abstract 开头的。在Java8之前,也就是接口中不能定义静态方法之前,相关的静态方法只能定义在单独的类中。比如,在java API中,Collection接口有一个对应的单独的类Collections。默认方法与原有的抽象方法不同的是,默认方法中有默认的实现,实现类可以改变实现或者不改变,这是为了便于增加接口的功能。
Java9中,静态方法和默认方法允许为private。这样可以方便在多个静态方法和默认方法中复用代码,也很好理解,跟之前了解的封装的特性有关,有点不明白为啥一开始在Java8中不这么搞。

抽象类

抽象类中可以包含抽象方法也可包含具体方法。抽象类和接口一样都不能用来直接创建对象,抽象类表达的是事物的概念
为什么一定要用抽象类?
我刚学的时候也很困惑,直接在具体类中用一个空方法体不就解决了吗?其实,使用抽象类是为了让使用者在继承的时候必须实现抽象方法,不能忽略,同样的,使用抽象类,就必须要用其非抽象类的子类去创建对象,而不是用功能有可能不完整的抽象类,这样做是为了减少失误,可以说是java的一种减少失误的机制。
抽象类跟接口很像,都不能直接创建对象,如果抽象类中全是抽象方法那就更像了!其实,他们是配合关系而不是替代。一般接口都会有一个对应的抽象类,比如,Collection接口对应AbstractCollection这个抽象类。在这个抽象类中全部or部分实现了接口中的所有抽象方法。在使用者角度,要用这个接口,可以选择直接实现接口,或者是继承它对应的抽象类,再根据需要进一步重写方法和实现抽象类中没有实现的抽象方法。有一种情况,如果要使用接口的这个类已经有继承的类了,那么根据单继承,只能实现接口中所有抽象方法。这里总算打通了。。

异常

程序正常退出是用return关键字,有正常就肯定有异常,异常退出就是用throw。return返回给上一级调用者,但throw之后执行的代码是不确定的,根据异常处理机制动态决定。比如空指针异常,程序在执行到某一行发现一个对象的值为null的时候,就不会继续执行,此时启动异常处理机制,首先创建一个异常对象,这里是NullPointerException的对象,然后从当前函数开始查找谁能捕获(处理)这个异常,当前函数没有就查看上一层,直到主函数,如果也没有就采用默认机制,即输出异常栈信息并退出,退出的意思就是异常点之后的代码是不会执行的。
异常类,基类是Throwable。它有两个直接子类:Error和Exception。Error是系统错误或资源耗尽 Java自己处理,应用程序中不需要抛出和处理。Exception有三个直接子类:IOException,RuntimeException,SQLException。所有异常中只有RuntimeException是未受检异常,其他都是受检异常。未受检异常表示程序逻辑错误,程序员应该检查bug而不是去想办法处理异常。受检异常是指程序本身没问题,是因为数据库等不可预测的错误导致的,调用者应该进行处理。
在catch块也可以重新抛出异常,这个抛出的异常可以是原来的,也可以是新的异常。
为什么要重新抛出异常呢?
因为当前代码不能完全处理该异常,需要调用者进一步再处理。为什么又要抛出新的异常?因为当前异常不太合适,不合适可能是信息不够,需要补充新的信息。也可能是过于细节,不便于调用者理解和使用,当然如果调用者想知道细节,可以通过getCause()来获取到原始异常。说到getCause()就要说到异常链,Throwable类的几个构造方法中,有两个主要参数,1.message,表示异常信息 2.cause,表示触发该异常的其他异常。上层的异常由底层触发,底层异常就用cause表示,这样就形成了一个异常链。在finally中最好不要写return或抛出异常。
总得来说,如果在你这一层捕获到异常,如果不能完全解决就向你的上级抛出,你的上级如果也不能完全解决,就应该把他拿到的信息和新的信息一并抛给更上级,最后直到大boss来进行负责和处理。每一级既不能掩饰问题,也不能逃避应该承担的责任。
暑假生活day8学习笔记
第7章 常用的基础类浪了几天终于开始看这一章了,估计笔记也不少了,可以发博客了吼吼~

包装类

java5之后引入自动装箱和拆箱的技术,可以直接将基本类型赋值给引用变量。自动拆箱和装箱是java编译器提供的,背后是替换为了对应的valueOf/xxxValue。所有的包装类中都重写了Object类中的equals,hashCode,toString方法。equals实际比较的是其包装的基本类型值,要注意只有两个float的二进制完全一致才返回为true。因为hashCode是根据对象来计算的,那么显然如果两个对象的equals相同时,那么hashCode必须一样,反之则不一定,因为可能产生地址冲突,比如会采用链地址法来解决(这个我猜的,具体可能用的其他办法)。子类重写equals必须重写hashCode,这个在很多地方都看到了,但现在不明白为啥这么规定,先记下来呗~
Integer中有一些二进制操作,如位翻转和循环移位,CPU能高效实现移位和逻辑运算。
Java内部采用UTF-16,提到这个就要说到Unicode,Unicode是为了解决全世界不同文字的问题而出现的,它给百万中字符编了码,但工作也只是编了码而已,并没有规定如何把编号映射为二进制。这也是高明之处,UTF-8,UTF-32,UTF-16等都是编码方式或者叫映射方式。int可以表示任意Unicode字符,整数编号在Unicode中称为代码点(code point),表示一个Unicode字符,还有一个相对应的叫代码单元(code unit)。好了,其实说的就是Character有一部分处理的就是有关代码点,代码单元,character,Unicode的事情。。。这部分我没细看,有点枯燥,先跳过了。。。
String类内部是用字符数组表示字符串的,String会根据参数创建新数组,把参数中内容复制到数组,并不会直接用参数的字符数组,一些有关操作也都是操作这个内部产生的数组。与包装类一样,String类也是不可变类,也不能被继承,内部char数组value也是final。String中那些看似修改的方法实际上是通过创建新数组来完成的。比如,concat()是创建新数组,然后把值赋值到这个新数组中来。当然,这样效率太低,所以一般使用StringBuilder和StringBuffer(线程安全)。String中有一个字符串常量池,通过常量的形式使用字符串时,使用的就是常量池中那个对应的String类型对象。String的+和+=,背后,Java编译器会生成StringBuilder,并转换为append。

泛型

泛型就是广泛的类型,将接口进一步延伸,使代码与它们能够操作的数据类型不再绑定到一起,同一套代码可以用于多种数据类型,复用代码,降低耦合,提高可读性和安全性,至于安全性后面仔细说。
泛型是类型参数化,处理的数据类型不是固定,而是可以作为参数传入。
一开始接触泛型也有这个困惑,为啥一定要用定义类型参数,定义普通类,直接使用Object不就行了。
其实,在内部,编译器会把泛型代码转换为普通非泛型代码,会将类型参数T擦拭掉,替换为Object,再加上必要的强制类型转换,之后java虚拟机执行的时候只知道普通类的代码。那回到问题,既然最后都是转换成Object,为啥要用泛型呢?因为可读性和安全性,比如,public class Pair{省略}.Pair pair = new Pair("哈哈",1);Integer id = (Integer)pair.getFirst()。这么写,编译没问题,但运行就会报错。此处若使用泛型,就能将错误避免,也有可读性:Pair pair =new Pair<>("哈哈",1)。
容器类
泛型类最常见的用途就是作为容器类,容器类就是容纳管理多项数据的类。
泛型方法
泛型方法跟它所在的类是不是泛型没关系,调用泛型方法时不需要特意指定类型参数的实际类型,编译器可以自动推断。
泛型接口
接口也可以是泛型的,在实现接口的类中指定具体类型。
在之前的泛型中,类型参数我们只能当做Object,其实可以限定这个参数的一个上界,上界可以是具体的类、具体接口、甚至其他类型参数。都使用关键字extends。

通配符

待续。。

文章主要是看了《Java编程逻辑》这本书,学习笔记,与大家分享一下~~
可以在评论去讨论噢~~

上一篇:Python之绘图和可视化


下一篇:MenuItem 显示中文乱码问题的解决方案