类变量、类方法、代码块、抽象类、接口以及内部类

        在了解完面向对象编程中的封装、继承、多态之后,最近接触到了几个新的知识点。在这里梳理一下,帮助自己复盘,同时也记录一下我的学习心得。

一、类变量和类方法

        同一个类的实例对象只是共享了一个模板原型,没有共享普通变量和普通方法,即各个对象的实例属性和普通方法是存放在独立空间里的,互不干涉。有继承关系的父子类也是如此,且属性的访问满足就近原则,方法的调用满足动态绑定机制。

//类变量定义
访问修饰符 static 数据类型 变量名;

//类方法定义
访问修饰符 static 数据返回类型 方法名(){}

        类变量和类方法(后面统称为类成员或静态成员)在类加载的时候就生成了,生命周期伴着类的加载而开始,随着类的消亡而结束。由于类的信息只加载一次,所以类成员也只加载一次。我们目前只需要知道它们被存放在堆中的一块区域上(JDK8以后),并且它们能够被该类的所有实例对象共享,即任何一个实例对象都能在满足访问修饰符条件的情况下访问到它们。

        但是这种访问是单向的,即实例对象可以访问到类成员,而类成员却访问不到实例对象里的普通属性和普通方法。我的个人理解是,因为类成员在类加载的时候就已经生成了,所以这个类的实例对象都能够知晓类成员的存放地址,故能访问到它们。但是实例对象是在类加载之后new出来的,类成员无法知晓它们的地址,故访问不到它们的普通属性和方法。不过类成员之间是可以互相访问的,因为它们存放在堆中的同一块内存区域上。

        由于类成员在类加载的时候就生成了,故它们可以直接通过类名调用(比如main方法),而不用像普通属性和普通方法一样,需要创建对象,然后通过对象来调用。当然,通过对象名调用类成员也是可以的,只要满足访问修饰符的条件,不过还是建议使用类名来调用。

        下面是几点注意事项:

        1、类变量不能在对象的构造器里赋值:因为类变量在类加载的时候就已经生成了,它的初始化只能是默认的初始化、定义的时候显式的初始化或者是在静态代码块(这个后面会讲)中进行初始化。构造器是初始化对象里面的成员,不能是类成员。

        2、类方法中不能使用this、super这种与对象有关的关键字,要明确类成员和对象之间的访问是单向的,类成员访问不到对象。

        3、类方法不能被重写:即类方法可以被继承,但不能被重写(覆盖)。

        在解释这个之前,需要明确重写(覆盖)到底是什么?

        重写从定义上来说,是子类重写父类的方法,要求方法名一致,参数也一致,访问修饰符不能被缩小,返回类型得是和父类返回类型相同的类或者子类。但是仅仅满足这个定义就可以了吗?如果是这样的话,对于父类的类方法,子类完全可以按照重写的定义写一个同名同参数的类方法出来,可是这个情况只能说子类的类方法把父类的类方法给隐藏了,而不是重写。

        重写不仅要从定义上满足规定,还需要具备和动态绑定机制结合的能力,即你“重写”的方法要能够满足动态绑定机制,才能称为重写,否则只能称为隐藏。重写是实现多态的前提,我们仔细想一想,所谓的方法多态、对象多态,其实最核心的就是父类的方法被子类重写(覆盖)了。Java的多态也称为Java的动态绑定技术,即只有在运行的时候才能知道被调用的是哪个方法,这就是所谓的多态。

        回到我们的开头:类方法可以被继承,但是不能被重写。当一个子类试图去重写父类的类方法,那么这个被“重写”的类方法不会参与到动态绑定机制当中,即“重写”的类方法(静态方法)不会和运行类型绑定,而是会像属性一样和编译类型绑定。

        Java从语法上是支持类方法的重写,但是运行效果上无法达到多态的目的。

        举个例子:

public class Test {
    public static void main(String[] args) {
        AAA a = new BBB();
        System.out.println(a.n);//输出5
        a.aaa();//输出 This is class AAA
    }
}

class AAA{
    int n = 5;
    static void aaa(){
        System.out.println("This is class AAA");
    }
}

class BBB extends AAA{
    int n = 10;
    static void aaa(){
        System.out.println("This is class BBB");
    }
}

        这足以说明类方法不能被重写,只能被隐藏。

        总结一下第三点:类方法可以被继承,但是不能被重写,只有普通方法的调用可以是多态的。类方法在类加载的时候就生成了,所以它只和编译类型相关,即编译类型决定了调用哪个类的静态方法,不执行动态绑定机制,即使子类的类方法和父类类方法同名同参数,这两个方法也没有关系,只能说子类的类方法把父类的类方法给隐藏了。

二、代码块

        代码块又称为初始化块,属于类中的成员,类似于方法。但是和方法不同的是,代码块没有方法名、返回值、访问修饰符和参数,只有方法体

        代码块不通过对象或者类显式调用,而是加载类或者创建对象的时候自动调用。相当于另一种形式的构造器,可以做初始化的操作。并且自动调用的顺序是:先调用代码块,再调用构造器(后面我们会对初始化调用的顺序做一个说明)。

        有两种代码块:普通代码块和静态代码块。

//普通代码块,“;”可以省略
{
};

//静态代码块
static{
};

        这里说一下静态代码块,前面有讲到静态方法,静态代码块类似于方法,所以静态代码块也是随着类的加载而生成,存放位置和静态成员是一样的,并且和静态方法一样,只能调用静态成员。不同的是静态代码块会自动执行(毕竟它是代码块嘛,代码块就是自动执行的),但是因为类的信息只需加载一次,所以静态代码块也只会执行一次。而普通代码块是每创建一个对象就执行一次。

        前面有提到“类的加载”,那么问题来了,类什么时候被加载?

        目前能想到的有三种情况:1、创建对象实例的时候;2、创建子类对象,父类的信息也会被加载;3、使用类的静态成员的时候。但是要注意的是,类的信息只会被加载一次。

        现在我们来捋一下一个对象初始化的步骤:

        在我前面的文章有说到,“在一个对象被创建的时候,首先是在JVM的方法区中加载这个对象所属类的相关信息,加载完成之后,在执行new语句时,会在堆内开辟一片空间,加载属性,这里的属性初始化分为三步,第一步是设定属性的默认值,即Int类型设为0,Boolean类型设为false,引用类型设为null等;第二步是看类定义当中是否有对属性进行赋值,如果有的话,则加载到堆中去;第三步是看是否有有参构造器,如果有的话,则将构造器里的内容赋值到属性上去。至此,可以认为完成了对对象的一个初始化。”

        在学完了代码块和静态属性之后,一个对象的初始化现在可以表述为:   

        创建一个对象的时候,第一步是设定静态属性的默认值,然后调用静态代码块和静态属性初始化(定义赋值)。这里要说明一下,代码块和属性初始化的优先级是一样的,执行的先后顺序取决于定义的时候的顺序。第二步是设定普通属性的默认值,然后调用普通代码块和普通属性初始化。第三步是调用构造器。

        对比一下这两种说法,其实只是在构造器之前多了一个代码块的调用,其他在本质上都是不变的。静态属性和静态代码块在类加载信息的时候就已经生成了,所以放在第一步。加载属性的步骤之前是:默认初始化、显式初始化、然后是构造器。现在多了一个代码块,且其优先级和显式初始化是一样的,故现在加载属性的步骤变成:默认初始化、显式初始化和代码块调用、然后是构造器。因为构造器是用来初始化对象的,和静态成员没有关系,故静态属性的初始化步骤只有前两个。

        如果是创建一个子类对象,那么会有什么不同呢?并没有,我们只要记得一点:创建一个对象实例,首先要完成类信息的加载,然后再完成对象实例的初始化。这两个步骤都是要按照父类到子类的顺序执行。所以创建一个子类对象,它们的静态代码块、静态属性初始化、普通代码块、普通属性初始化,构造方法的调用顺序如下(这里的属性初始化是指显式初始化,默认初始化永远都是最先调用的):
    1、父类的静态代码块和静态属性初始化(父类信息加载)
    2、子类的静态代码块和静态属性初始化(子类信息加载)
    3、父类的普通代码块和普通属性初始化,然后父类构造器(父类初始化)
    4、子类的普通代码块和普通属性初始化,然后子类构造器(子类初始化)

三、抽象类

        在介绍抽象类之前,先介绍一下抽象方法。

        抽象方法就是:当一个父类的某些方法需要声明,但是又不确定如何实现,这个时候就可以将其声明为抽象方法,即没有方法体的方法,空的{}也不可以。然后这个抽象方法由子类来实现。包含抽象方法的类就可以声明为抽象类。

//抽象类A
abstract class A{

//抽象方法eat,不能有方法体。
public abstract void eat();

}

          抽象类中不一定要包含抽象方法, 但是抽象方法只能存在于抽象类中。所以如果一个抽象类中有抽象方法,那么继承它的子类要么把抽象方法都实现,要么也声明为抽象类。抽象类的价值更多地在于设计。

     一般来说,抽象类需要被继承,抽象方法需要被重写。所以在我们后面学到一些修饰词,要注意不能和这两点相悖。举个例子,我们上面讲到了static静态方法,但是抽象方法可以被static修饰吗?不行,因为static方法不能被重写,而抽象方法又需要被重写。

        几个注意事项:

        1、abstract只能修饰类或者方法。

        2、抽象类不能实例化:类是对对象的具体描述,而抽象类不具体,我们无法生成一个不具体的对象。比如我们可以new一个苹果,但是不能new一个水果。并且我们不能对抽象类中的抽象方法准确地分配内存,所以编译器规定了不能实例化一个抽象类。

        3、能否实例化和有无构造方法没有关系:构造方法是初始化对象的,new关键字是向JVM申请内存来创建对象的,所以抽象类可以有构造方法。(这点其实也可以从抽象类一般是需要被继承的来想,普通类继承了抽象类,那么普通类的初始化之前肯定要先完成父类的初始化,所以抽象类是可以有构造方法的。换句话说,如果一个类没有构造器或者构造器私有化,则该类不能被继承。

四、接口

        接口就是给出一些没有实现的方法,封装到一起,到某个类要使用的时候,再根据具体情况把这些方法实现,可以用于规范代码。

        在JDK7.0之前,接口里的所有方法都没有方法体,即都是抽象方法。

        在JDK8.0之后,接口里可以有静态方法、default关键字修饰的默认方法和抽象方法,即接口中可以有方法的具体实现,但只局限于这三种方法。

        接口中的所有方法默认都在前面加上了public abstract,属性都默认在前面加上了public static final。(final关键字是起到"保护"作用的,修饰类保护类不能被继承,修饰方法保护方法不能被重写,修饰属性保护属性不能被修改(但是final修饰的属性得在初始化的时候就完成赋值,因为初始化之后这个属性就不能被修改了~)。)

interface 接口名{
	//属性(必须要初始化,因为是final)
  	//方法(三种:抽象方法、默认实现方法、静态方法)
}

class 类名 implements 接口名{
    //自己属性;
    //自己方法;
    //必须实现的接口的抽象方法(因为抽象方法不能存在于普通类中,所以必须实现,除非用抽象类去implement)
}

       接口和抽象类,implements接口和extends类名的辨析:

        1、接口本质上可以看做一个抽象类,比如接口也不能被实例化,但是接口只能包含抽象方法、默认方法以及静态方法,而抽象类可以包含所有类型的方法。而且接口的属性已经做了限制,必须是public static final,所以接口不能有构造器(可以从静态属性初始化不需要用到构造器这点去理解),而抽象类是可以有构造器的。

        2、实现接口和继承一个类:

        首先,实现接口和继承父类的类,都能访问到接口和父类的属性和方法,在这点上实现接口和继承是一样的。并且,接口可以指向实现了接口的类,即接口可以是多态的,它也具备了动态绑定机制,同时,接口的关系就像继承关系那样是可以传递的。

        但是实现接口和继承一个类的不同点在于:实现接口只需要满足like-a的关系,而继承一个类需要满足is-a的关系。可以把实现接口看成是单继承机制的一个补充。并且一个类可以同时实现多个接口,即单继承,多实现

        还有一点要说明的就是:接口中不能有静态代码块

        对此可以从两个方面去理解:(1)接口是一个抽象的概念,其不能有实现代码,这就意味着接口不能包含构造器、代码块;(2)静态代码块是在类加载的时候执行的,但是接口不是类,因此它不能被加载,也就不能执行静态代码块。

            

五、内部类

        顾名思义,一个类的内部完整的嵌套了另一个类,被嵌套的类就称为内部类,嵌套内部类的类就称为外部类,和外部类平行的类我们称为外部其他类。

        根据内部类定义的位置,可以分为两大类:

        1、定义在外部类的局部位置中(比如方法内、代码块、构造器里):

         在这里又可以根据有无类名分为两类:

         1.1局部内部类(有类名):它的本质还是一个类,只不过定义的位置是在外部类的局部位置中,并且它的地位是一个局部变量,所以不能有访问修饰符去修饰它,比如public class A,这是不允许的,但是像final这种修饰词就可以,因为final本身就可以用来修饰局部变量。并且它的作用域只存在于定义它的代码块里。

        1.2匿名内部类(没有类名)这里的没有类名指的是没有我们能够直接看到的名字,比如我们平常定义class A,那么A就是我们能看到的类名。但是JDK在底层是有给匿名类分配名字的,只是我们不知道而已。匿名类的使用场景多存在于,有些类我们只需使用一次,那么这个时候就可以使用匿名内部类。它的基本语法如下:

//参数列表会传递给构造器

new 类或接口(参数列表){
    
    类体;//如果是new抽象类或者接口的话,那么里面的抽象方法必须实现。
};//分号不能少,毕竟本身就是一个创建对象的语句。

         相当于extends这个类或者是implements这个接口,然后马上new了一个实例出来。即匿名的内部类是这个类的子类(这个接口的实现类)

        匿名内部类使用一次之后就不能再使用了,因为它没有名字,我们找不到它。但是已经new出来的匿名内部类的实例,是可以被反复使用的。可以理解为类的模板只能使用一次,但是这个一次性模板塑造出来的对象可以反复使用。

        在使用的时候,可以把匿名内部类当做实参直接传递,形参就是匿名内部类的父类(其实现的接口),这样代码看起来就比较简洁高效。

        匿名内部类和上面提到的局部内部类是一样的,地位是一个局部变量,不能有修饰符。

        并且,我们就可以很容易地分析出:(1)这两个内部类可以直接访问外部类的所有成员,因为它们是外部类的一部分,而我们前面提到的四种访问修饰符,最小的限制范围就是本类。(2)如果外部类要访问这两个内部类,可以在方法中创建内部类的对象,然后调用方法即可。(3)当外部类和这两个内部类的成员重名的时候,默认遵循就近原则。如果想访问外部类的成员,可以通过“外部类名.this.成员”去访问,这里的“外部类名.this”指的就是调用内部类所在方法的外部类的对象。有点拗口,不过还是比较好理解的。

        2、定义在外部类的成员位置上

        根据有无static修饰可以分为以下两类:

        2.1成员内部类:它的定位就是一个成员,所以可以添加任意的访问修饰符,作用域是整个类。同样,它可以直接访问外部类的所有成员,当外部类要访问成员内部类的时候,同样需要在方法里创建内部类的对象,然后调用这个方法。(毕竟类里除了属性以外,其他语句只能存放在方法体{}里面)

        外部其他类想要调用内部类有两种方式:

第一种:

class Outer{
	class Inner{
        
    }
}

Outer out = new Outer();
Outer.Inner inner = out.new Inner();//也可以不写第一句,直接写成new Outer().new Inner()

相当于把new Inner()看成out的一个成员

第二种:在Outer里写一个方法,返回Inner的实例

class Outer{
	class Inner{
        
    }
    public Inner returnInner(){
        return new Inner();
    }
}
Outer out = new Outer();
Outer.Inner inner = out.returnInner();

        如果内部类和外部类的属性重名了,依旧遵循就近原则,和前面提到的局部类一样。

        2.2静态内部类

        使用static修饰,可以直接访问外部类的所有静态成员,但是不能访问非静态成员。(用静态成员的角度去理解静态内部类)所以静态内部类的成员如果和外部类的成员重名,想要访问外部类中重名的成员,可以使用“外部类名.成员”去访问。(这里因为静态内部类能访问到的只能是静态成员,所以通过类名访问

        外部类访问静态内部类也有两种方式,和访问成员内部类相比,除了在第一种方法上有一点不同,第二种方式是一模一样的。在这里只对第一种方法进行说明。

第一种:

class Outer{
	satic class Inner{
        
    }
}

Outer.Inner inner = new Outer.Inner();

        静态内部类和成员内部类的不同之处体现在static这个关键字上,除了访问受限,静态类还和别的静态成员一样,可以直接通过类名访问,故在第一种方法上和成员内部类有些不同。

        简单了解了四种内部类之后,我们再来深挖一下内部类和静态成员的关系。

        大家可以去试一下,在除静态内部类之外的三种内部类中,不能声明任何一个静态成员,而在静态内部类中却可以这样声明。这是为什么呢?

        先来分析非静态内部类:

        它们的地位就好比是外部类的普通属性、或者是代码块中的局部变量。对于普通属性或者局部变量而言,它们依赖于外部类的实例,即只有外部类实例化之后,它们才会生成。对于非静态内部类也是如此,即只有外部类实例化之后,非静态内部类才能被加载乃至实例化。

        所以如果非静态内部类中有静态成员,我们知道,静态成员是在类加载的时候就生成,其调用不需要创建具体的对象,直接通过类名调用即可。可是因为非静态内部类的加载受制于外部类的实例化,所以在外部类没有实例化的情况下我们不能通过非静态内部类.静态成员调用静态成员。所以,非静态内部类中不能有静态成员。

        再来分析一下为什么静态内部类中可以有静态成员:静态内部类可以看成是外部类的静态成员,而静态成员在外部类加载的时候就会被加载,所以静态内部类的加载不依赖于外部类的实例化,这个时候使用静态内部类.静态成员则是可行的,故静态内部类中可以有静态成员。

       但内部类中允许有static final属性,这是因为编译器底层做了优化,对static final修饰的属性的调用,不会导致类的加载。既然类不会加载,那么我们前面说的那些问题就不会出现,所以这样是允许的。

        四种内部类到这里就已经讲完啦~撒花!

        其实对于这四种内部类,只要把握好以下4点:1、内部类的本质依然是类;2、内部类根据定义的位置有不同的角色定位,可能是局部变量,也可能是类的成员,在把握好内部类的角色定位的基础上,对于外部类的访问、外部其他类的访问的相关问题也就迎刃而解了。3、静态内部类要把握的就是静态成员的一些“限制”和“特权”。4、匿名内部类看似复杂,其实只要理解它的语法,就很好懂啦~

        至此,我们已经把类的五大成员都已经介绍完了,分别是属性、方法、构造器、代码块、内部类。

        如有讲错的地方,恳请大家批评指正!

        

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值