深入类和对象——深入Java基础系列(二)

前言

本文从类和对象的基本概念出发,一方面,深入探讨了内部类,通过对比的方式介绍了不同类型的内部类的定义、特点、使用场景以及使用时的注意事项,更进一步了解了内部类相关特性的内在原理。另一方面,详细的介绍了类的整个生命周期,彻底弄清楚类从加载到消亡的全过程。


目录

前言

1. 概述

1.1 类和对象的概念

1.2 抽象类和内部类

1.3 深入理解内部类

2. 类的生命周期

2.1 加载

2.2 连接

2.3 初始化

2.4 使用

2.5 类卸载

3. 对象的生命周期


1. 概述

1.1 类和对象的概念

(1)类:类是对某种类型的事物的公共属性和行为的抽取,他并不是实际存在的实体。

(2)对象:对象是类的一个实例,代表现实世界中可以明确标识的一个实体

可以这么理解:在现实世界中我们会用“高富帅”、“白富美”描述一种群体,这只是一种描述而没有具体的指向。而志玲姐姐是个白富美,她就是个实体例子,也就是对象。

(3)匿名对象

  • 匿名对象:没有引用类型变量指向的对象称作为匿名对象。  
  • 实例:使用 java类描述一个学生类。  new Student(),左边并没有引用类型变量指向这个对象;
  • 匿名对象要注意的事项:  

a. 我们一般不会给匿名对象赋予属性值,因为永远无法获取到。 

b. 两个匿名对象永远都不可能是同一个对象。 new 了两次,地址肯定不一样
  • 匿名对象好处:简化书写。  
  • 匿名对象的应用场景:  
a. 如果一个对象需要调用一个方法一次的时候,而调用完这个方法之后,该对象就不再使用了,这时候可以使用匿名对象。  new Student().study();
b. 可以作为实参传入构造函数,装饰者模式中经常用到;

所以,类可以看作是一个模板,而对象则是类的一个实例(类看作是一张图纸,对象则是按照图纸生产的产品)。

1.2 抽象类和内部类

(1)抽象类

抽象类是不能实例化的类,abstract关键字修饰class,其目的主要是代码重用。除了不能实例化,形式上和一般的Java类并没有太大区别,可以有一个或者多个抽象方法(没有方法体,用abstract修饰),也可以没有抽象方法。抽象类大多用于抽取相关Java类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。

  • 什么时候用抽象类:

描述一类事物的时候,发现这类事物确实存在着某种行为,但是目前这种行为是不具体的,这时候应该抽取这种行为的声明,而不去实现该种行为,这时候这种行为我们把它称为抽象的行为,这时候应该使用抽象类。具体的行为在其子类中实现。

  • 抽象类要注意的细节
a. 如果一个方法没有方法体,那么该方法必须使用abstract修饰。    
b. 如果一个类含有抽象方法,那么这个类肯定是一个抽象类或者接口。
c. 抽象类不能创建对象。
d. 抽象类是含有构造方法的。  
e. 抽象类可以存在非抽象方法与抽象方法。  
f. 抽象类可以不存在抽象方法。  
g. 非抽象类继承抽象类的时候,必须要把抽象类中所有抽象方法全部实现。言外之意,抽象类继承抽象类可以不全部实现。
  • 关于abstract的细节

a. abstract 不能与static共同修饰一个方法。static修饰的方法能被类直接调用,而abstract修饰的方法没有方法体,有冲突。

b. abstract 不能与private共同修饰一个方法。 abstract修饰的方法,非抽象类继承抽象类必须实现所有的抽象方法,私有不能被继承
c. abstract不能以final关键字共同修饰一个方法。final 修饰的方法不能被重写,这与抽象方法需被子类实现相违背。

(2)内部类

在类的内部定义的另一个类叫做内部类,使用内部类的好处:

  • 实现多重继承;
/*我们知道,在java中一个类可以多重实现,但不能多重继承。但有时候我们确实是需要实现多重继承,
  例如:我们即继承了父亲的行为和特征也继承了母亲的行为和特征。
  那么我们有没有方法解决多重继承的问题呢?内部内就提供了一种曲线实现多重继承的方式
*/
    public class Father {
        public int strong(){
            return 9;//强壮指数
        }
    }
    public class Mother {
        public int kind(){
            return 8;//善良指数
        }
    }
    //子类通过内部类实现多重继承
    public class Son {
        //继承Father的内部类
        class FromFather extends Father{
            public int strong(){
                return super.strong() + 1;
            }
        }
        //继承Mother的内部类
        class FromMother extends  Mother{
            public int kind(){
                return super.kind() - 2;
            }
        }
        public int getStrong(){
            return new FromFather().strong();
        }
        public int getKind(){
            return new FromMother().kind();
        }
    }
  • 内部类可以很好的实现隐藏:一般的非内部类,只能用public和default,是不允许有 private 与protected权限的,但内部类可以;
  • 减少了类文件编译后的产生的字节码文件的大小;(内部类在编译完成后也会产生.class文件,文件名称是:外部类名称$内部类名称.class)

使用内部类的缺点:程序结构不清楚

内部类又分为:成员内部类、局部内部类和匿名内部类。

 定义特点注意事项使用场景
成员(实例)内部类在一个类的成员位置定义另外一个类,那么另外一个类就称作为成员内部类。

成员内部类的访问方式:

方式1: 在外部类内提供一个方法创建内部类的对象进行访问。

 

方式2: 在其他类创建内部类的对象进行访问。 创建的格式: 外部类.内部类  变量名 = new 外部类().new 内部类();

a. 成员内部类可以直接访问外部类成员(包括成员变量和成员方法)。

b. 如果成员内部类与外部类存在同名的成员,在内部类中默认是访问内部类的成员。成员通过“外部类.this.成员”指定访问外部类的成员。

c. 如果成员内部类出现了静态的成员,那么该成员内部类也必须使用static修饰。

d. 如果成员内部类是私有的,那么创建内部类的对象就只能在外部类提供方法创建。

每一个外部类对象都需要一个内部类的实例,内部类离不开外部类存在(相当于心脏对人体)。
局部内部类在一个类的方法内部定义另外一个类,  另外一个类就称作为局部内部类。用在方法内部,作用范围仅限于该方法中。在方法内部创建。

a. 如果局部内部类访问了局部变量,那么该变量需要使用final修饰。

b. 不能使用private,protected,public修饰符

c.局部内部类可以直接访问外部类成员

如果内部类对象仅仅为外部类的某个方法使用,使用局部内部类。
匿名内部类没有类名的类,匿名内部类就是一种局部内部类

匿名内部类的格式:

 new 父类(父接口){

            匿名内部类的成员;

};

a.  必须存在继承或者实现关系,默认继承或实现new后面的类型

b. 匿名内部类没有名字,所以没有构造函数

简化内部类的使用,在整个操作中只使用一次的话

1.3 深入理解内部类

(1)为什么内部类可以访问外部类的成员?

这篇博文通过反编译内部类的字节码, 说明了内部类是如何访问外部类对象的成员的

关于内部类如何访问外部类的成员, 分析之后其实也很简单, 主要是通过以下几步做到的:

1 编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同, 这个成员变量就是指向外部类对象的引用;(非静态内部类对象有着指向其外部类对象的引用

2 编译器自动为内部类的构造方法添加一个参数, 参数的类型是外部类的类型, 在构造方法内部使用这个参数为1中添加的成员变量赋值;

3 在调用内部类的构造函数初始化内部类对象时, 会默认传入外部类的引用。

                                                              ——引自 https://blog.csdn.net/weixin_39214481/article/details/80372676

(2)为什么局部内部类(包括匿名内部类)访问了局部函数的形参,该变量需要使用final修饰?

  • 首先要明确:成员函数的形参是也局部变量。
  • 没有访问的局部函数的形参,是不用修饰的。
  • 并不是所有的局部变量都需要被final修饰,只是被内部类引用的局部函数形参才需要被final修饰。

内部类并不是直接调用方法传进来的参数,而是内部类将传进来的参数通过自己的构造器备份到了自己的内部,自己内部的方法调用的实际是自己的属性而不是外部类方法的参数。  

这样理解就很容易得出为什么要用final了,因为两者从外表看起来是同一个东西,实际上却不是这样,如果内部类改掉了这些参数的值也不可能影响到原参数,然而这样却失去了参数的一致性,因为从编程人员的角度来看他们是同一个东西,如果编程人员在程序设计的时候在内部类中改掉参数的值,但是外部调用的时候又发现值其实没有被改掉,这就让人非常的难以理解和接受,为了避免这种尴尬的问题存在,所以编译器设计人员把内部类能够使用的参数设定为必须是final来规避这种莫名其妙错误的存在。”

                                                                                   ——引自:https://www.cnblogs.com/jlustone/p/7517323.html

2. 类的生命周期

一个java的源文件,经过编译后生成后缀名为.class的文件,即字节码文件,java虚拟机就识别这种文件,Java程序的生命周期就是class文件从加载到消亡的过程。

鱼骨图中从尾巴开始,类的生命周期分为加载、连接、初始化、使用和卸载五大过程。

2.1 加载

字节码文件并不是本地的可执行程序,当运行Java程序时,首先运行JVM,然后再把字节码文件加载到JVM中运行。

类的加载过程其实就是将字节码文件中的二进制数据读入到内存中,首先Java虚拟机找到需要加载的class文件,并把类的信息加载到“运行时数据区”的方法区中,然后在堆区中(堆在JVM启动时就创建)实例化一个java.lang.Class对象,用来封装类在方法区内的数据结构,并作为方法区中这个类的信息的入口(每个类都是Class类的实例)。

(1)类加载器

类的加载工作由类加载器来完成,它负责读取.class文件并转换成java.lang.Class类的一个实例,加载到内存中,JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构。

  • BootStrap(启动类加载器): 加载jdk/jre/lib/rt.jar(开发的时候使用的核心jar包);
  • ExtClassLoader(扩展类加载器):加载jdk/jre/lib/ext/*.jar(扩展包);
  • AppClassLoader(应用程序加载器):加载CLASSPATH中的jar包和class文件;

 

 

public abstract class ClassLoader
public class SecureClassLoader extends ClassLoader
public class URLClassLoader extends SecureClassLoader
//AppClassLoader和ExtClassLoader都继承于URLClassLoader
static class AppClassLoader extends URLClassLoader
static class ExtClassLoader extends URLClassLoader

除了启动类加载器Bootstrap ClassLoader,其他的类加载器都是ClassLoader的子类。Bootstrap ClassLoader使用C++写的。

Application ClassLoader的Parent是Extension ClassLoader,而Extension ClassLoader的Parent为Bootstrap ClassLoader。加载一个类时,首先BootStrap进行寻找,找不到再由Extension ClassLoader寻找,最后才是Application ClassLoader。(这里的Parent并不是继承体系,而是委派体系)

(2)类加载器特征

  • 全盘负责

一个类A是由一个类加载器加载的,如果A中关联(继承或包含)到其他的非系统类,那么类B也是由该类加载器加载

  • 类加载器双亲委托加载

机制:如果一个类加载器收到一个类加载请求,该加载器不会自己去尝试加载这个类,而是把这个请求转交给父类加载器,先委托其父类加载器,如果还有父类加载器就继续委托,直到没有父类加载器为止,最顶层的类加载器(启动类加载器)就需要真正的去加载指定类,如果在其类目录中找不到这个类,继续往下找,找到发起者类加载器为止,若找不到则报ClassNotFound错误。

双亲委派的好处:防止有些类被重复加载,有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。

2.2 连接

一般会跟加载阶段和初始化阶段交叉进行,这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,过程由三部分组成,即验证、准备和解析三步:

(1)验证

当类被加载之后,必须要验证一下这个类是否合法,比如该类是否符合字节码格式规范,变量与方法是不是有重复、数据类型是不是有效,继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被jvm所运行。

(2)准备

准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值(在方法区分配)对于非静态的变量,则不会为它们分配内存(实例化变量在对象实例化步骤中分配内存)。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。jvm默认的初值如下:

  • 八种基本数据类型默认的初始值是0
  • 引用类型默认的初始值是null
  • 有static final修饰的会直接赋值,例如:static final int x=10;则默认就是10。

(3)解析

这一阶段的任务就是把常量池中的符号引用转换为直接引用,就是jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址(通过内存地址才能直接找到符号指向的内容)。

2.3 初始化

这个阶段就是将静态变量(类变量)赋值的过程,即只有static修饰的才能被初始化,执行的顺序就是:父类静态域或静态代码块,然后是子类静态域或者子类静态代码块;(在一个类中的静态内容则按照顺序执行)

2.4 使用

在类的使用过程中依然存在三步:对象实例化、垃圾收集、对象终结。这三个过程也就是对象的生命周期。

(1)对象实例化

  • 在堆区分配对象需要的内存

  分配的内存包括本类和父类的所有实例变量,但不包括任何静态变量

  • 对所有实例变量赋默认值

  将方法区内对实例变量的定义拷贝一份到堆区,然后赋默认值

  • 执行实例初始化代码

  初始化顺序是先初始化父类再初始化子类,初始化时先执行实例代码块(非静态语句块)然后是构造方法

  • 如果有类似于Child c = new Child()形式的c引用的话,在栈区定义Child类型引用变量c,然后将堆区对象的地址赋值给它

(2)垃圾收集

当对象不再被引用的时候,就会被虚拟机标上特别的垃圾记号,在堆中等待GC回收

(3)对象的终结:对象被GC回收后,对象就不再存在,对象的生命也就走到了尽头

注意:静态变量和静态代码块是在初始化过程中赋值和执行的,所以它是优先于非静态成员存在于内存中(在new之前,静态变量就已经被赋值,静态代码块就被执行了),当父类和子类的所有静态内容执行完了之后,再执行父类非静态代码块和构造函数,最后执行子类的非静态代码块和构造函数。

2.5 类卸载

即类的生命周期走到了最后一步,程序中不再有该类的引用,也就是说类所会被JV对应的Class对象没有被引用的时候,JVM就会执行垃圾回收,从此生命结束。

3. 对象的生命周期

清楚了类的生命周期,而对象的生命周期则在类的生命周期之中,也就是类的使用阶段。下面两篇博文对相关内容分析的非常到位,值得反复阅读:

理解Java类加载器(一):Java类加载原理解析

https://blog.csdn.net/justloveyou_/article/details/72217806

深入理解Java对象的创建过程:类的初始化与实例化

https://blog.csdn.net/justloveyou_/article/details/72466416

从上面博文中注意几个问题:

(1)对象创建的过程:

当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的实例变量及其从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序猿的意志进行初始化。

在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化、实例代码块初始化 以及 构造函数初始化

总的来说,类实例化的一般过程是:父类的类构造器<clinit>() -> 子类的类构造器<clinit>() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值