类的高级概念

86 阅读14分钟

类的高级概念

static

  • static关键字 -- 是一个可选修饰符,它本身的含义叫做“静态”。 它不仅只能修饰方法,它也可以修饰:属性、代码块、内部类 。

    • 大家需要关注的是:我们学过的是属性、方法 和 构造方法。
    • 那么static可以修饰其中两个:属性和方法不能修饰构造方法。另外今天还会给大家增加能够在类里面书写的两个内容:代码块(初始化块)以及内部类。
  • 对于我们来说,类里面总共可以书写的内容是5个:属性、构造、方法、初始化块和内部类。

    其中:前三个是我们要重点掌握的,也是我们最常操作的。

    初始化块在大型项目当中可能会在部分关键类里面出现,所以大家要有一定的认知度。

    而内部类,今天只是让大家先看看,知道这个东西,暂时不用去管它咋用咋设计。

    以后,我们遇到需要使用它的时候,还会再给各位同学讲解。

  • 所以今天我们总共要学习的内容就是3个:

    • static 关键字的作用与意义,特别是用来修饰属性和方法,与不用static修饰有什么样的区别。包括:语法上的区别、运行的效果区别、内存的区别以及设计理念的区别; --- 今天整个学习的关键点
    • 初始块的学习,这是我们在类里面能够书写的第4种内容。这个新知识点后面会部分常用。当然这里也会讲解有static和没有static修饰的区别。 --- 今天学习的次重点;
    • 内部类的学习 --- 目前来说,对各位同学没有掌握的要求,知道了解即可,重在保留资料,有待后面学习和领会。

static 与 非static 的属性

  • 在一个类当中,属性也可以选择使用static进行修饰。

    • 语法上的对比:

      有static

      • 有static修饰的属性可以用"类名. "的方式直接访问,也可以通过“对象. ”的方式进行访问(但第二种方式,不推荐)。
      • 用static修饰的属性,是全类的所有对象共享一个值,无论是该类的哪个对象把这个值改了,那么大家的值都跟着变。

      非static

      • 非static修饰的属性是只能用“对象. ”的方式访问;
      • 非static修饰的属性是每个对象身上有一个该属性的变量,存放了该对象该属性的值。
    • 从两者的第2点语法对比,我们领会Java语法设计者的设计意图。

    • 之所以让static修饰的属性能够用“类名.”的方式去访问,不是为了让我们方便,而是为了体现这个属性是全类共享的。

    • 而非static修饰的属性用"对象."的方式去访问,是为了清楚的表达到底要操作是哪个对象的该属性。

    • 在一个类当中,能够用static修饰的属性是不多见的,通常都需要在某个特定的问题域限制当中才找得到。static才是特例,通常都是非static的属性。

  • 根据经验,只有常量,我们能够不动脑袋直接设计为static

  • 原因:

    • 常量属性的值是在类的定义中给定的,每个对象身上都是同样的值(常量也不能改);所以没有必要在每个对象身上存放一份,只需要全类共享一个就可以了。
    • 常量属性的值在类定义中给定以后,外部也没有更改的权利,所以可以不用保密,直接public公布给外部看;
    • 所以,通常常量属性的修饰符是:public static final的。

    千万不要为了方便去把属性设计为static,一定要找到全类共享一个值的属性才做这种设计。

  • 内存上的区别:

    • 存放的位置不同 非static修饰的属性是在对象本身身上,也就是在堆当中; static修饰的属性没有存在对象身上,也就不是在堆当中;

      而是单独作为类的共享信息存放在一个叫做“静态区”的内存空间里;一个类的一个static属性只划分一个存放空间。

    • 该属性在内存中产生的时机不同:

      非static属性是在产生对象的时候,在内存中产生; static的属性是在 加载期 产生的于内存中。

    我可以给大家一个简单的总结,无论是现在我们说的static修饰属性,还是下午要讲的static修饰方法 或 代码块(初始化块)。

  • 我给你两个凡是:

    • 凡是用static修饰的内容,都是表示它与对象无关,只与相关; 所以:用static修饰的属性,我们又叫做 “类属性”/“静态属性” , 没有static修饰的属性,我们一般叫做“成员属性
    • 凡是用static修饰的内容,都会在加载期搞事情。

static 和 非static修饰的 方法

  • 在语法上的对比:

    • 有static的方法:

      • 在调用该方法的时候,可以通过“类名.”的方式来访问,也可以通过"对象."的方式来访问;只不过第2种方式不推荐,但不会报错;

      • 在方法的内部实现中,静态方法只能调用本类的静态属性 或 静态方法;之所以不能调用的原因:

      原因1:

      静态方法是通过"类名."的方式调用,这个时候方法内部是没有当前对象这一目标,也就是说没有“this”,所以无法确认这里访问的非静态属性或方法到底是哪个对象的。 --- 这是站在面向对象的设计思想来表达的;

      原因2:

      当我们在进行类的编译和加载的时候,在进行加载的顺序划分时,JVM都是首先加载静态的,然后再加载非静态的。如果在加载静态内容时,发现里面有非静态的调用,这个时候JVM是不认识这些非静态内容的,因此报错。反过来,在加载非静态的方法时,由于静态内容已经加载过了,所以JVM认识,因此通过。

    • 非static的方法:

      • 在调用该方法的时候,只能通过“对象.”的方式访问。
      • 在方法的实现内部,即可以操作本类的静态属性和静态方法,也可以调用到本类的非静态属性和非静态方法;

这样的设计同样表明了用static修饰的方法是跟对象无关的;非static的方法是一定要确定到底是哪个对象去做这件事情。所以,用static修饰的方法又被称之为 “类方法”或“静态方法” ;非static修饰的方法被称为 “成员方法”

  • 设计上的区别: 我们要把工具类的工具方法,设计为static的;其他的都是非static的。

    场景: 学生类有一个行为,这个行为时下楼帮胡老师买烟。

    如果这个场景中,学生只是需要实现买烟本身,然后把结果返回给调用者胡老师,整个过程中不会改变买烟的这个学生对象的任何数据或状态,那么学生类里面的任何对象去做这件事情,对调用者胡老师来说是没有差异的。那么在这种情况下,我们就可以把学生类的买烟行为设计为static的了。

    但假如在这个场景中,学生下楼买烟,然后胡老师说期末给你加五分。这就不一样了。这个时候,必须明确行为的执行者是哪个学生对象,这个学生对象的数据值或状态会受买烟行为的影响。那么这种情况下,我们必须把这个行为设计为非static的,因为它跟执行对象有关了。

static 一定不能用来修饰 构造方法

为什么? 构造方法是拿来干啥的? --- 创建对象 而static天生就是用来表示与对象无关。

static 和 非static修饰 初始化块

对初始化块的简单认知

初始化块是我们可以在一个类里面定义的第4个内容。它的语法非常简单,就是直接在类里面,打上一对"{}",然后在"{}"里面书写代码。

它从外观形式上非常像一个没有方法声明,只有方法实现的特殊语句块。在它内部,我们可以书写任意指令语句,包括:输入输出、变量声明、运算表达、流程控制; 由于这个代码块是没有方法声明的,也不能接受参数和返回,所以很明显它是不能够在类的外部随意调用的。 它的执行只能是在一个特定的时间点运行。 什么时候呢? 产生对象的时候,所以它也被称之为“实例初始化块”。每产生一个对象,就会被执行一次。

实例初始化块到底在初始化一些什么呢?如果初始化对象的话,那么它的作用就和我们的构造方法重叠了。是的,确实是有这种情况,因此我们要探讨两个东西:

  • 先后问题? 首先调用到构造方法,构造方法就会默认执行三个动作, 先在内存中划分空间(对象存放的空间); 然后在该空间划分属性; 接下来就会先执行我们在实例初始化块当中书写的代码; 再对属性进行初始化(这里的初始化是指有没有在声明属性的时候用代码赋初始值); 完了以后再执行我们在构造方法当中书写的代码。

  • 哪些初始化动作放在初始化块,哪些动作是放在构造方法里面做?

    • 构造方法里面的初始化,主要做的事通过外部调用者传入的参数,给当前对象的属性赋值;
    • 如果某些属性不需要从外部接受参数赋初始值,那么我们更多的选用的是,在声明属性的时候直接使用“=”去赋值;
    • 而实例初始化块更多的作用不是用来给对象中的属性赋初始值的,而是用来在一些特殊场景当中执行非赋值的初始代码,比如:开启资源,开启通讯管道,开启线程、开启文件等等。

初始化块也分static 和 非static的

  • 前者也叫“静态初始化块”,后者叫做“实例初始化块”

"静态初始化块"是在加载类的时候被执行的,而一个类在JVM当中只需要加载一次,所以它是首先被执行,且以后无论产生多少对象,甚至不产生对象,也不再被执行。

“实例初始化块”是在产生对象的时候被执行,且产生多少个对象就会被执行多少次。

如果说“实例初始化块”的执行时机和次数与构造方法有一定的重叠,导致它的使用量很少。但是“静态初始化块”由于其独特的执行时机和执行次数,导致它是没有被替换的可能的。

很多资源的操作,包括开启呀,预加载呀,都是一次性完成的,没有必要每new一个对象就做一次,而且很多时候我们会把做这些动作放到程序的加载期而不是运行期,这是为了提升程序的运行速度。所以,在以后的使用当中,你们肯定会遇到书写“静态初始化块”的时机,但很多人将除了运气不会被面试官问以外,再也看不到操作不到“实例初始化块”。

  • 静态初始化块也满足两个凡是:

    • 与类有关; --- 类加载的时候被执行 与对象无关;--- 不能在其内部访问对象的非静态内容,没有this当前对象,与产生对象与否无关。
    • 加载期搞事情 --- 它搞的事情是在加载期被执行

内部类

故名思义 -- 就是在一个类A的内部定义另外一个类B,那么类A就是外部类,类B就是A的内部类。

  • 首先需要明确:

    • 内部类一定要定义在外部类的"{}"里面,而不是简单的写在一篇java文件当中;写在一篇java文件当中,它们两个是平行关系,不是嵌套关系。

    • 内部类除了定义的位置有特殊性以外,它在本质上和普通的类没有区别; 包括:

      a、仍然拥有属性、构造、行为、初始化块、甚至是它的内部类;

      b、内部类虽然和外部类在同一篇java文件当中定义,但是编译以后它拥有自己独立的class文件。 对应的面试题是:是否一篇java文件编译后生成一篇class文件? 回答:这是错误的说法。应该是一个类一篇class文件。

      c、内部类的类名,不是我们书写的简单名,而是和外部类的类名共同组成的。 对应的面试题是:class文件的名字是否与定义的类名保持一致? 回到:这是错误的说法。普通类是一致的,但是内部类不一致,内部类的class文件名要先添加上外部类的名字和“$”符号,后面再跟上内部类的名字。

内部类的分类

首先根据内部类书写的位置,分为两大类:

  • 成员内部类; 如果一个内部类是直接定义在外部类的“{}”当中,它的位置和外部类的属性、方法、构造,都处于平行位置,那么就叫做“成员内部类”。

    当然作为外部类的成员,它是可以有访问修饰符的,用来控制这个内部类是否可以被外部使用。

    成员内部类编译后的class文件名:外部类名字$内部类的名字

  • 局部内部类; 如果一个内部类是定义在外部类的某个方法当中,它的位置和这个方法的局部变量保持一致,那么就叫做“局部内部类”。 作为方法的内部定义,跟局部变量一样出了方法就认为消失不在了,所以它只能在这个方法内部使用,不能有访问修饰符。

    局部内部类编译后的class文件名:外部类的名$序号内部类名字 这里序号从1开始,是同名局部内部类编译的顺序序号。

  • 再根据语法特殊性,对“成员内部类”和“局部内部类”再分类:

    • 成员内部类分为:普通成员内部类 和 静态内部类; 区别就在于 加不加 static 关键字;
    • 局部内部类分为:普通的局部内部类 和 匿名内部类 所谓“匿名”就是没有给这个类起类名,匿名内部类是在定义的时候马上产生对象,只能使用一次。

四种内部类的使用语法

  • 普通的成员内部类 要想用普通的成员内部类,首先要先产生外部类的对象,然后用"外部类对象.new" 的语法 去产生内部类对象。

    OutClass1 out = new OutClass1();
    OutClass1.InnerClass1 inner1 = out.new InnerClass1();
    
  • 静态内部类 要用静态内部类,由于有static修饰符,所以这个静态内部类与它所属的外部类对象无关,因此可以不需要产生外部类对象,直接用外部类类名访问。

    OutClass1.InnerClass2 inner2 = new OutClass1.InnerClass2();
    
  • 局部内部类 局部内部类只能在定义它的方法之内使用。

    InnerClass3 inner3 = new InnerClass3();
    

    内部类当中书写this,代表的是当前的内部类对象,如果要表示它所关联的外部类对象,那么要写成“外部类类名.this”; 局部内部类的方法里面,能够操作到外部类的属性,但是不能操作到所属方法的局部变量(只能访问,不能改人家的值,当成常量用)。

  • 匿名内部类 匿名内部类是在new对象的同时,去定义这个对象里面的属性和行为,然后由于没有给这个类型取名字,所以只能用这一次。

     new Object(){
             private int a;
             public void test(){
    ​
             }
         }.test();
    ​
    

    在new对象的同时,去定义这个对象里面有哪些属性和行为。由于这个对象有了新增的属性和行为,所以它不属于new关键字后面那个类型了,而是属于一种新的具有a属性和test方法的类型,但这种类型没有取名字。