一、 继承
1.1 重写父类的方法
当子类覆盖了父类的方法后,子类的对象无法访问父类中被覆盖的方法,但可以在子类方法中调用父类中被覆盖的方法(怎么调用呢?可以使用super关键字或者父类类名作为调用者来调用父类中被覆盖的方法)。
尤其注意:覆盖和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法,例如,如下的代码会引发编译错误:
publicclass Super
{
public static void fly(){
System.out.println("I amflying!");
}
}
publicclass OverSuper extends Super
{
public void fly(){
System.out.println("I amRunning!!!");
}
public static void main(String args[]){
OverSuper o = newOverSuper();
o.fly();
}
}
OverSuper.java:3: 错误: OverSuper中的fly()无法覆盖Super中的fly()
public voidfly(){
^
被覆盖的方法为static
如果父类方法的访问权限尾private,说明该方法对子类是隐藏的,因此子类无法访问该方法,也就是无法重写该方法。如果在子类中定义了一个与父类private具有相同方法名、相同形参列表、相同返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新的方法。例如,下面的代码并不是重写方法,所以可以增加static关键字,代码完全正确。
class BaseClass
{
private void test(){...}
}
classSubClass extends BaseClass
{
public static void test(){...}
}
方法的重载(overload)与重写(override)有什么区别?重载主要发生在同一个类的多个同名方法之间,而重写则发生在子类和父类的同名方法之间。
1.2 super的限定
this和super关键之都不能出现在static修饰的方法中。Static修饰的方法是属于类的,该方法的调用者可能是一个类,而不是对象,因而this和super的限定也就失去了意义。
如果在构造器中使用super,则表示该构造器初始化的是该对象从父类得到的实例变量,而不是该类自身定义的实例变量。
在继承的过程中,实例变量同实例方法一样,也会被隐藏,父类中的实例变量访问方法和实例方法的访问方式一样,通过super来实现。
classBaseClass
{
int a = 5;
}
classSubClass extends BaseClass
{
int a = 7;
public void accessOwner(){
System.out.println(a);
}
public void accessBase(){
System.out.println(super.a);
}
}
classSuperLimit
{
public static void main(String args[])
{
SubClass s = new SubClass();
s.accessOwner();
s.accessBase();
}
}
如果被覆盖的是类变量,则在子类的方法中可以通过父类名作为调用者来访问。
1.3调用父类构造器
在一个构造器中使用this调用同一个类中重载的构造器,在子类构造器中使用super调用其父类的构造器。使用this和super调用构造器时都必须出现在构造器的第一行,这也是为什么 this和super不会同时出现的原因。
classBase
{
public String name;
public double score;
public Base(String name, double score){
System.out.println("父类中的有参构造器");
this.name = name;
this.score = score;
}
public Base(){ //此处的无参构造器不能省略,否则会报错
System.out.println("父类中的无参构造函数");
}
}
class Subextends Base
{
public String coures;
public int age;
public Sub(String name, double score,int age){
super(name, score); //通过super来调用父类的构造器
System.out.println("③");
this.age = age;
}
public Sub(String coures){
//这里会追溯到父类的无参构造器
System.out.println("①");
this.coures = coures;
}
public Sub(String coures, int age){
this(coures); //通过this调用同一个类中的另一个重载构造器
System.out.println("②");
this.age = age;
}
}
publicclass AccessBase
{
public static void main(String args[]){
Sub s1 = newSub("QS_Che",100,23);
Sub s2 = newSub("Java",28);
System.out.println(s1.name +" " + s1.score + " " + s1.age);
System.out.println(s2.coures+ " " + s2.age);
}
}
值得注意的是,不管是否使用super调用父类构造器的初始化代码,子类构造器总会调用父类构造器一次。调用有如下几种情况。
① 子类构造器执行体的第一行使用super显式调用(系统将根据super里传入的参数列表匹配父类构造器)。
② 子类构造器执行体的第一行使用this显式调用本类中重载的构造器,系统将会根据this里的实参列表调用本类中的另一个构造器。被调用的另一个构造器会调用父类构造器(如果本类中的另一个构造器没有使用super显式地调用父类的构造器,则该子类中的构造器会调用父类的无参构造器。如上面代码所示,在编程时应该注意,父类中不能省略无参构造器)。
③ 子类构造器中this和super都没用使用时,系统也会在执行子类构造器之前,隐式地调用父类的无参构造器。
当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前之执行,直到追溯到最终父类为止。因此,创建任何Java对象,最先执行的总是java.lang.Obect类的构造器。
再看一个构造器之间调用关系的例子:
classCommon
{
public Common(){
System.out.println("Common的无参数构造器");
}
}
classAnimal extends Common
{
public Animal(String name){
System.out.println("Animal带一个参数的构造器,name为" +name);
}
public Animal(String name, int age){
this(name);
System.out.println("Animal带两个参数的构造器,其age为" +age);
}
}
publicclass Wolf extends Animal
{
public Wolf(){
super("灰太狼",3);
System.out.println("Wolf里的无参构造函数");
}
public static void main(String args[]){
new Wolf();
}
}
输出:
Common的无参数构造器
Animal带一个参数的构造器,name为灰太狼
Animal带两个参数的构造器,其age为3
Wolf里的无参构造函数
二、 多态
为什么会有多态?Java引用变量有两个类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由运行时实际赋给该变量的类型决定。如果编译时类型和运行时类型不一致时,就可能出现所谓的多态性。
classBaseClass
{
public int book = 6;
public String s = "父类字符串";
public void base(){
System.out.println("父类的普通方法");
}
public void test(){
System.out.println("父类被覆盖的方法");
}
}
classSubClass extends BaseClass
{
public String book = "JAVA EE应用实践";
public String s = "子类字符串";
public void sub(){
System.out.println("子类的普通方法");
}
public void test(){
System.out.println("子类覆盖父类的方法");
}
}
publicclass MultiState
{
public static void main(String args[]){
BaseClass bb = newBaseClass();
bb.base();
bb.test();
SubClass ss = new SubClass();
ss.sub();
ss.test();
//此处体现了对象的多态性
BaseClass bs = newSubClass();
bs.base(); //此时好比父类的对象调用base方法
bs.test(); //此时好比子类的对象调用test方法
//bs.sub(); //此条语句编译不能通过,因为bs的编译类型是BaseClass,而此处编译时调用的却是子类的方法
System.out.println(bb.book);
System.out.println(ss.book);
System.out.println(bs.book);
System.out.println(bs.s); //实例变量不具备多态性,因此,此处输出的是父类的实例变量
}
}
当把一个子类对象直接赋给父类引用变量时(BaseClass bs = new SubClass();),这个引用的编译类型是父类,而运行时类型是子类,当运行期间调用该引用变量的方法时,其方法行为总是表现出子类方法行为的特征。
与方法不同的是,对象的实例变量不具备多态性,正如程序中输出的book和s变量时,并不是输出子类里定义的变量,而是输出父类里的变量。
三、 封装
封装从字面上来理解就是包装的意思,专业点就是信息隐藏,是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。系统的其他对象只能通过包裹在数据外面的已经授权的操作来与这个封装的对象进行交流和交互。也就是说用户是无需知道对象内部的细节(当然也无从知道),但可以通过该对象对外的提供的接口来访问该对象。
对于封装而言,一个对象它所封装的是自己的属性和方法,所以它是不需要依赖其他对象就可以完成自己的操作。
使用封装有三大好处:
① 良好的封装能够减少耦合。
② 类内部的结构可以自由修改。
③ 可以对成员进行更精确的控制。
④ 隐藏信息,实现细节。
四、 块初始化
初始化块的修饰符只能是static,使用static修饰的初始化块称为静态初始化块。
当创建java对象时,系统总是先调用该类里定义的初始化块,如果一个类里定义了两个普通的初始化块,则前面定义的初始化块先执行。
初始化块虽然也是java类的一种成员,但他没有名字,也就没有标识,因此,无法通过类、对象来调用初始化块。初始化块只是在创建java对象时隐式执行,而且在执行构造器之前执行。
class Root
{
static{System.out.println("Root的静态初始化块");}
{System.out.println("Root的普通初始化块");}
publicRoot()
{
System.out.println("Root的无参构造器");
}
}
class Mid extends Root
{
static{System.out.println("Mid的静态初始化块");}
{System.out.println("Mid的普通初始化块");}
publicMid()
{
System.out.println("Mid的无参构造器");
}
publicMid(String msg)
{
this();
System.out.println("Mid的有参构造器,msg的值为:" +msg);
}
}
class Leaf extends Mid
{
static{System.out.println("Leaf的静态初始化块");}
{System.out.println("Leaf的普通初始化块");}
publicLeaf()
{
super("English");
System.out.println("执行Leaf的构造器");
}
}
public class InitialBlock
{
publicstatic void main(String args[]){
newLeaf();
newLeaf();
}
}
静态初始化块也被称为类初始化块,也属于类的静态成员,同样需要遵循静态成员不能访问非静态成员的规则,一次静态初始化块不能访问非静态成员,包括不能访问实例变量和实例方法。
与普通初始化块类似的是,系统在类初始化阶段执行静态初始化块时,不仅会执行本类的静态初始化块,而且还会一直追溯到java.lang.Object类。先执行Object类的静态初始化块(如果有),然后以此类推,最后才执行该类的静态初始化块。