Java笔记6
在上一篇中,虽然写到了继承,但是并没有详细深入。这篇来看看相关问题。
继承共同行为
先做出如下假定:有个游戏有多个角色,包括剑士,魔法师等,每个角色都有血量,名字,等级,攻击方式等。
观察发现,每个角色都有名字,血量,等级三个共同特点,那么就可以将这部份提升为一个父类role(我觉得基类可能是个更好的名字)。然后剑士,魔法师这些子类(派生类)继承这些共有的属性,扩展自己的方法(攻击方式)。
重新定义行为
进一步观察,发现每个角色都要攻击,那么就可以把“攻击”这个方法(fight()
)提取到父类中。然后在子类中重写(Override)这个方法。父类中的fight()方法实际是空的,具体的攻击行为留到子类中重写。我这里写一个调用fight()方法
的方法:
1 | static void drawFight(Role role){ |
我将传入的参数定义为Role类型,这样不管我传入的时剑士还是魔法师类型都可以。这就是多态的表现形式。剑士是(is-a)Role类型。在重写这个方法时,如果一不小心写错了,例如fight()写成了Fight(),那就不是重写了。可以在重写的上一行添加标注@Override
。它会检查该方法是否重新定义了某方法。
抽象方法和抽象类
在上面的例子中,父类中的fight()方法中是空的,问题是如果不使用子类的fight()方法就会出现没有攻击行为的情况。这种情况下可以把fight()理解为一个抽象方法,在前面加上abstract
关键字。这表示这个方法是不完整的,同时,这个类也是不完整的,因此类也要加上abstract
。当一个子类继承抽象类时,要么自己也保持抽象类,要么就是将这个抽象方法重写完整。这里只是简单提一下,具体细节在下篇讨论。
protected成员
对于继承来说,protected是非常重要的。protected的成员相较于默认的多了个子类范围,即可以在同一个类中,同一个包中,同一个或不同个包的子类中访问。
重写的一些小细节
前面写过,可以用super来调用父类的东西,但是要注意的是不能调用private的父类。同时,重写还要注意“两同两小一大”的规则,权限只能扩大不能缩小。在JDK5之前,返回类型是必须相同的,但在5之后可以相同也可以更小。即可以返回一个父类对象或一个子类对象。
在林的书中说:static的方法属于类所有,在子类中定义了一个相同的static方法,这个方法属于子类,而不是重写。我觉得这句话有错,重写的方法本身就不是父类的,它就是子类的方法,父类的是父类的,两者不产生任何影响,只是说子类的会将父类的“盖”住,导致调用的时候会用到子类的。
能否继承private
参看链接https://blog.csdn.net/zhousenshan/article/details/51222908
再论==和equals
==
当比较的数据是基本数据类型,==在两个数据大小一致时返回true,即使两个数据的类型并不一致。
当比较的是引用对象时,==只有在两个引用变量指向同一个对象时才返回true。同时,==不可以用于比较类型上没有父子关系的两个对象。
equals
本以为equals就是用来比较的是对象的值,但实际上还是有点区别的。首先,在Object中,equals和==的效果是一样的,都是比较两个是否是同一个对象(比较内存地址)
参考链接:https://blog.csdn.net/snowqoo/article/details/2455605,https://www.cnblogs.com/www123----/p/7857298.html,https://www.cnblogs.com/yangzhilong/p/6391548.html
toString方法
有如下代码(不完整)
1 | Person P = new Person("孙悟空"); |
当答应一个对象时,会返回“类名+@+hashCode”值,这是调用了toString()方法。它是一个特殊的方法,用于在打印对象时输出“自我描述”信息。System.out.println(p);
相当于System.out.println(p.toString());
。但是原本的返回值的信息太简略了,可以重载Object类的toString()方法。
类成员
在Java类中只能包含成员变量,方法,构造器,初始化块,内部类(包括接口,枚举)五种成员,static可以修饰除构造器之外的四种,用static修饰的成员属于类,而不属于某个对象。
1 | public class NullAccessStatic{ |
单例(Singleton)类
在某些时候,不需要根据类创建多个对象,只需要一个。
1 | class Singleton { |
首先需要将原来的构造函数隐藏起来,防止自由创建对象。在类中提供一个类变量存储对象的缓存,然后提供一个静态的方法用来在缓存为空时生成对象,如果缓存不为空,就将这个缓存返回。
final修饰符
final关键字可用于修饰类,变量,方法,表示它们不可改变。final的变量获得了初始值后就不可改变,不是不可赋值。变量类型可以是类变量,实例变量,局部变量,形参。
final成员变量
成员变量是随着类初始化或对象初始化而初始化的,因为final的变量一旦赋值后就不能改变,所以必须在初始化时指定初始值,不然初始化结束后里面就是0,“\u0000”,false,null等初始值,完全没有意义。
对于它们的使用,最关键的就两点:可以在哪指定初始值以及指定后就不能再改变。
- 类变量:必须在静态初始化块或声明该变量时指定
- 实例变量:必须在非静态初始化块,声明该变量或构造器中指定。
final局部变量
并没有什么特别的,要注意,作为形参时,其值应由传入的参数决定。
final修饰基本类型和引用类型
当修饰引用类型时,它仅仅强调引用的地址,即指向的对象不变,而对象本身内的改变与之无关。
可执行宏替换的final变量
- 使用final修饰符
- 定义final变量初始值时指定了初始值
- 该初始值可以在编译时就确定下来
满足以上条件的变量将变成一个直接辆,比如final int a=5
后,再使用a时,会将a直接替换为5。
final方法
final修饰的方法时不能被重写。但有种特殊的情况:
1 | public class PrivateFinalMethodTest{ |
private修饰的父类方法,对于子类来说是不可见的,因此也就不存在重写父类方法。
对于final修饰的方法仅仅是不能被重写,但是可以重载。
final类
对于final类,它不能被继承。
不可变类(immutable)
不可变类的意思是该类的实例变量被创建后就不可改变。Java的八个包装类和String类都是不可变类。要创建自己的不可变类要遵循如下规则:
- 使用private和final来修饰该类的成员变量
- 提供带参数构造器,用于根据传入参数来初始化类里的成员变量
- 仅提供getter方法,普通方法是无法修改final的变量的
- 如有必要,重写hashCode()和equals()方法。equals()方法根据关键成员变量来作为两个人对象是否相等的标准,除此之外,还要保证两个用equals()方法判断相等的对象的hashCode()也相等。
例如String这个类,它是根据String对象里的字符序列来作为相等的标准,其hashCode()方法也是根据字符序列计算得到的。下面给出例子:
1 | public class Address { |
前面写过,final仅仅能表示修饰的引用变量的指向不变,其对象还是可以任意改变的。如果,在一个不可变类中定义了一个final的成员变量是引用类型的,那么这个不可变类就失败了,它可变。例:
1 | class Name{ |
按上面的代码我可以随便修改Person对象p的值。请看下面修改的:
1 | package test1; |
现在就无法修改p的值了,原理在于,原来p的name就是n的值,当修改n时,p的name也被改变。现在,在创建p时,调用构造函数后会创建一个临时Name对象,把传进来的name的值(即具体内容,而不是将引用/地址)给这个临时对象,然后将临时对象赋给p的name。在以后修改n时,不会对p有任何影响,因为n和临时变量只有数值上的传递。
缓存实例的不可变类
不可变类由于一旦创建就不可更改,所以可以方便的进行共享,应该考虑缓存这个实例。缓存的实现很多,这里先考虑用数组实现。
1 | package cacheImmutale; |
1 | package cacheImmutale; |
分析上面的代码,根据CacheImmutale
类创建的对象是一个不可更改的String字符串name,这个类有一个数组,用于保存输入的name。有两个细节要注意,第一:cache[pos++] = new CacheImmutale(name);
pos++是先把pos的值传出去在自己加1,所以相当于cache[pos] = new CacheImmutale(name);pos=pos+1;
注意不是将name的值赋给第一个。第二:为什么valueOf要返回cache数组中的一个?因为cache数组是CacheImmutale[]类型的,里面都是CacheImmutale类的对象,要把这个对象指向c1和c2,如果name相同,也就没有新建对象,c1和c2指向的是同一个对象。
Integer类采用的就是类似的策略,如果用new,则每次都返回全新的对象,如果是valueOf()方法,则会启用缓存策略,但只缓存-128~127之间的。