TJI读书笔记10-复用类

时间:2022-08-05 00:43:06

代码复用是java众多牛逼哄哄的功能之一(好像OOP语言都可以呢…),代码复用的理想状态是,使用类但是又不破坏现有代码. 当然住了复制粘贴以外还有其他的方法. 一个叫组合,一个叫继承.

组合语法

其实刚开始我非常不理解什么叫组合. 后来才知道,在一个类中置入一个对象的引用就可以了. 学术范儿就是这样,把一些经常使用的司空见惯的东西搞出一个高大上的理论来.
类的成员变量会被默认初始化. 但是,编译器不是简单的对所有类的成员的引用变量都进行默认初始化(如果你显式初始化了它,那就不会再默认初始化了)因为这会带来不必要的开销.(是这样的吗,我怎么记的初始化那一章不是这么说的呢…)
初始化引用可以在以下四个位置进行:

  • 定义对象的地方
  • 类的构造器中
  • 正要使用这些对象之前
  • 使用实例初始化器
class Soap {
private String s;
Soap() {
System.out.println("Soap()");
s = "Constructed";
}
public String toString() { return s; }
} public class Bath {
private String // Initializing at point of definition:
s1 = "Happy",
s2 = "Happy",
s3, s4;
private Soap castille;
private int i;
private float toy;
public Bath() {
System.out.println("Inside Bath()");
s3 = "Joy";
toy = 3.14f;
castille = new Soap();
}
// Instance initialization:
{ i = 47; }
public String toString() {
if(s4 == null) // Delayed initialization:
s4 = "Joy";
return
"s1 = " + s1 + "\n" +
"s2 = " + s2 + "\n" +
"s3 = " + s3 + "\n" +
"s4 = " + s4 + "\n" +
"i = " + i + "\n" +
"toy = " + toy + "\n" +
"castille = " + castille;
}
public static void main(String[] args) {
Bath b = new Bath();
System.out.println(b);
}
} /* Output:
Inside Bath()
Soap()
s1 = Happy
s2 = Happy
s3 = Joy
s4 = Joy
i = 47
toy = 3.14
castille = Constructed
*///:~

又一个新概念,{ i = 47; }这玩意儿叫实例初始化器,这不就是一个初始化块吗…

继承语法

说到继承,所有的类都是继承自Object的. 继承使用的时候,通过extends关键字实现.继承可以是子类对父类的一个扩展.

初始化基类

继承不只是复制基类的接口,当创建了一个子类实例的时候,该对象包含了一个基类的子对象. 这个子对象跟直接通过基类创建的对象是一毛一样的. 二者的去被是,子类创建的基类的子对象被包装在了子类的内部.
对于子类中基类的正确初始化是直观重要的. java有个机制可以保证这一动作的正确执行.那就是在子类的构造器中自动的插入对基类构造器的调用. 因为基类的构造器具有执行基类初始化所需要的所有能力.
通过这段代码就可以看出来.

class Art{
public Art(){
System.out.println("Art()");
}
} class Drawing extends Art{
public Drawing(){
System.out.println("Drawing()");
}
}
public class Cartoon extends Drawing{
public Cartoon(){
System.out.println("Cartoon()");
}
public static void main(String[] args) {
new Cartoon();
}
}/*output:
Art()
Drawing()
Cartoon()
*/

像下面这段代码就是有问题的.

class Art{
int i;
// public Art(){
// System.out.println("Art()");
// }
public Art(int i){
System.out.println("Art("+i+")");
}
} class Drawing extends Art{
int j;
// public Drawing(){
// System.out.println("Drawing()");
// }
public Drawing(int j){
System.out.println("Drawing("+j+")");
}
}
public class Cartoon extends Drawing{
public Cartoon(){
System.out.println("Cartoon()");
}
public static void main(String[] args) {
new Cartoon();
}
}

也就是说,java只是自动的帮你在子类中插入基类的无参构造器.针对只有带参数的构造器或者想制定使用带参数的基类的构造器的时候,就必须用过super显示调用.

class Art{
int i;
// public Art(){
// System.out.println("Art()");
// }
public Art(int i){
System.out.println("Art("+i+")");
}
} class Drawing extends Art{
int j;
// public Drawing(){
// System.out.println("Drawing()");
// }
public Drawing(int j){
super(j);
System.out.println("Drawing("+j+")");
}
}
public class Cartoon extends Drawing{
public Cartoon(){
super(10);
System.out.println("Cartoon()");
}
public static void main(String[] args) {
new Cartoon();
}
}/*output:
Art(10)
Drawing(10)
Cartoon()
*/

代理

上面说了两种关系,组合和继承. 那么有一种介于组合和继承之间的,我们成为代理. 为什么说介于两者之间呢,首先,我们要在新构建的类中,创建某个类的实例. 同时,将该实例的接口暴露出去.
比如有一个车载电脑CarControl. 如果让一个Car继承Control这显然是不合理的.但是简单的靠组合又不能达到理想的效果那就可以使用代理的方式.

public class CarControl {
void speed(int speed){
System.out.println("speed up to"+speed);
}
void turbo(boolean open){
System.out.println("turbo opened:"+open);
}
}

通过代理的方式来构建一辆车

public class Car {
private CarControl controler = new CarControl();
private String name; public Car(String name) {
this.name = name;
} public void carSpeed(int speed) {
System.out.println(name + " speed up!");
controler.speed(speed);
} public void carTurbo(boolean open) {
System.out.println(name + " turbo state...");
controler.turbo(open);
} public static void main(String[] args) {
Car bmw = new Car("bmw");
bmw.carSpeed(100);
bmw.carTurbo(true);
}
}

我第一次看到这种方式的时候,想到的第一个问题就是,这玩意儿有毛用? 后来想了一下,第一,可以隐藏被代理对象(底层)的接口. 第二,可以提供更灵活的控制方式. 我们可以选择使用被代理对象的某个子集. 也可以用来增强被代理对象的功能. 另外,在使用别人的类库的时候,有种很蛋疼的情况,万一别人的类是final的,但是我们又想对别人的方法做出一些”增量”的改变.那代理就是一个好办法了.

final关键字

一句话,final就是不可改变的. 不想改变有连个理由,设计和效率.

final的数据

许多语言都有这么一个机制来告诉编译器,这一块数据是不能改变的. 数据很定很有用:

  • 一个永不改变的编译时常量
  • 一个在运行时被初始化的值,而你不希望它再改变
    对于编译常量而言,可以在编译时执行计算式,减轻运行时的负担. 对于java而言,这种编译时常量,必须是基本数据类型. 并且以final表示. 这种用法,在定义的时候,就应该被赋值.

按照剧本,一个static final的变量(也就是所谓的编译时常量),将用大写表示,而且每个单词用下划线分隔.

有一个有意思的事情

import java.util.Random;

public class FinalAndStatic {
static Random rand = new Random(47);
static final int s1 = rand.nextInt(100);
final int s2 = rand.nextInt(100); public static void main(String[] args) {
FinalAndStatic fas1 = new FinalAndStatic();
System.out.println("s1="+fas1.s1);
System.out.println("s2="+fas1.s2);
//===============================================
FinalAndStatic fas2 = new FinalAndStatic();
System.out.println("s1="+fas2.s1);
System.out.println("s2="+fas2.s2);
}
}/*output:
s1=58
s2=55
s1=58
s2=93
*/

注意static final和final的区别. static声明为静态变量的时候,由于是类所属的,所以永远只占一块内存区域,而且再声明为final之后,这个值就不能变了. 也就达到了所谓的”编译时常量”的效果. 但是final仅仅是不可改变的意思. 也就是初始化之后就不能再改变了. 但是如果我再new一个新实例的时候,他还是会在实例的内存区域占有一席之地. 仅仅是在新new出来的这块内存上不可变.想想其实这是显而易见的事情,但是,刚开始我是困惑的,加上埃大爷的话太学术范儿,一时没理解.

空白final
java允许空白final,简单说,就是仅仅允许一次显式初始化. 也就是说下面这样的代码是允许的.

final int i;
i=10;

final的参数

形参也是可以被final修饰的. 意思就是这个方法可以读取这个形参,但是不能修改. 这个特性主要用来向匿名内部类传递参数.(等看到的时候再说吧.现在有点懵…)

void foo(final String str){
...
}

final的方法

final的方法主要有两个原因. 第一,锁定它,不允许被重写. 第二,所谓效率. 早期java中,final的方法会将方法的调用转为内嵌调用. 正常方法的执行是,将参数压入栈,跳至方法代码处执行,然后跳回,清理栈中的参数,处理返回值. 但是内嵌调用是将方法体中的实际代码拷贝插入.这回消除正常调用的额外开销,但是代码会膨胀. 那到底哪种方式效率更高,说不好. 但是现在的jvm可能已经不针对final方法做这种所谓的内嵌调用的优化了. 这些底层的优化机制应该交给编译器和jvm去处理,我们使用final的唯一考量点就是,不想让方法被重写.

final和private关键字
一个private的方法,隐式的被指定为final了. 因为private的方法在类的外部是看不到的. 那子类中的重写更是无从谈起.

final的类

一句话fianl的类不可以被继承. 那final的类中的方法的潜台词也是带final.同样的道理,这个类都没法去继承了,怎么重写它.

初始化和类的加载

简单来说,程序的执行过程就是,加载-初始化-执行
java的每个类的编译代码都是存在它自己的一个独立的文件中. 该文件只有需要使用程序代码的时候才会被加载. 也就是说类的代码是在初次使用的时候才加载. 初次使用有两种情况,第一,创建类的第一个对象时. 第二,需要访问static域或者static方法的时候.

如果一个类继承自另外一个类,那么会先加载基类再加载其自身. 如果这个基类还有基类,会先加载基类的基类,一直找到根基类Object为止.

乱七八糟不知道怎么归类的知识点

关于清理
java中没有一个严格意义上的析构函数,对象的销毁是交给垃圾回收机制的. 所以更多时候我们更倾向于忘掉清理这回事儿. 但是,有时候在一个类的生命周期里,有些我们还是有必要手动去清理的. 需要注意的是,清理的动作尽量要放在finally子句之中保证它一定会去执行. 还有就是不要装X的写在finalize()里,尽量去构建自己的清理方法.

名称屏蔽
据说C++中会出现名称屏蔽这个现象,我的C++仅停留在helloword的水平,所以我也不知道是不是这样. 但是埃大爷说是这样的…
简单来说,对重载而言,子类中定义的方法,并不会对基类中的任何版本产生屏蔽.比如,我在基类中有个foo(int i)foo(String str) 那么我再在子类中定义一个foo(boolean flag)那这三个方法对子类而言都是可用的.

class YoYoYo{
public void foo(int i){
System.out.println(i);
}
public void foo(String str){
System.out.println(str);
}
}
public class OverLoadTest extends YoYoYo{
public void foo(boolean flag){
System.out.println(flag);
}
public static void main(String[] args) {
OverLoadTest olt = new OverLoadTest();
olt.foo(1);
olt.foo("yoyo");
olt.foo(true);
}
}

protect关键字
protect就一句话吧,比private权限高,比default权限低. 也就是,只有类内部及子类可以访问

向上转型
子类是基类的一种类型. 继承可以保证基类中的所有方法在子类中同样可以使用,换句话说,所有能够发给基类的消息都可以发送给子类. 那么向上转型就是安全的. 也就是说子类是基类的一个超集.但是,向上转型之后,可能会丢失子类中定义的成员. 因为他只能看到基类那个子集里的东西了.