Java 面向对象
一、面向对象和面向过程的区别
面向过程编程(Procedural-Oriented Programming,POP)和面向对象编程(Object-Oriented Programming,OOP)是两种常见的编程范式,两者的主要区别在于解决问题的方式不同:
- 面向过程编程(POP):面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
- 面向对象编程(OOP):面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。
相较于 POP,OOP 开发的程序一般具有下面这些优点:
- 易维护:由于良好的结构和封装性,OOP 程序通常更容易维护。
- 易复用:通过继承和多态,OOP 设计使得代码更具复用性,方便扩展功能。
- 易扩展:模块化设计使得系统扩展变得更加容易和灵活。
在选择编程范式时,性能并不是唯一的考虑因素。代码的可维护性、可扩展性和开发效率同样重要。
求圆的面积和周长的示例,简单分别展示了面向对象和面向过程两种不同的解决方案
面向对象:
1 | |
定义了一个 Circle 类来表示圆,该类包含了圆的半径属性和计算面积、周长的方法。
面向过程:
1 | |
直接定义了圆的半径,并使用该半径直接计算出圆的面积和周长。
二、面向对象三大特征
封装
封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
就是把抽象出的数据【属性】和对数据的操作【方法】封装在一起,数据被保护在内部,程序的其他部分只有通过被授权的操作【方法】,才能对数据进行操作
步骤
- 对属性进行私有化private 【不能直接修改属性】
- 提供一个公共的 public set 方法,用于对属性判断并赋值 public void
- 提供一个公共的 public get 方法,用于获取属性的值
1 | |
构造器 与 set 结合
将 set 方法写在构造器中,仍然可以进行数据验证

继承(Extends)
继承可以解决代码复用。当多个类存在相同的属性和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有子类无需重新定义这些属性和方法,只需要通过extends来声明继承父类即可。
子类自动拥有父类定义的属性和方法。
基本语法
1 | |
重要规则
- 子类继承了所有的属性和方法,但是私有属性和方法不能在子类直接访问,要通过父类提供的公共方法去访问;
- 子类必须调用父类的构造器,完成对父类的初始化;
- 当创建子类对象时,不管使用子类哪个构造器,默认情况下总会去调用父类的无参构造器(
super(););(示例1) - 如果父类没有无参构造器,则必须在子类的构造器中使用
super去指定使用父类哪个构造器完成对父类的初始化工作,否则编译不通过;(示例2) - 如果希望指定去调用父类的某个构造器,则显式地调用一下:
super(参数列表); super在使用时,必须放在构造器第一行;(super只能在构造器中使用)super()和this()都只能放在构造器第一行,因此这两个方法不能共存在一个构造器- 父类构造器的调用不限于直接父类!将一直往上追溯直到
Object类(顶级父类) - java 是单继承机制:子类只能继承一个父类;
- 不能滥用继承关系,子类和父类之间必须满足 is-a 的逻辑关系。(Person is a Music? Music extends Person × Cat is a Animal? Cat extends Animal √ )
示例2-1

示例2-2

原理分析

分析:
(1)首先看子类是否有该属性;
(2)如果子类有,并且可以访问,则返回信息;(父类相同的属性就无法访问了)
(3)如果子类没有,就看父类有没有,如果父类有,并且可以访问,就返回信息;
(4)如果父类没有,就按照(3)的规则,继续找上级父类,知道 Object……
super
super代表父类的引用,用于访问父类的属性、方法、构造器。
调用父类的构造器的好处:分工明确,父类属性由父类初始化,子类属性由子类初始化。
基本语法
访问父类的属性(除private以外),super.field;
访问父类的方法(除private以外),super.method();
访问父类的构造器,super(参数列表); (只能放在构造器第一句,只能出现一句)
重要规则
当子类中有和父类中的成员(属性和方法)重名时,为了访问父类的成员,必须通过
super。如果没有重名,使用super、this、直接访问是一样的效果。- 查找同名方法和属性的规则:
- 先找本类,如果有,则调用
- 如果本类没有,找上一级父类(有,并可以调用,则调用)
- 如果父类没有,继续往上找,直到
Object类 method()和this.method()等价遵循以上规则,super.method()则直接跳过本类查找父类
super的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用super去访问爷爷类的成员;如果多个基类(上级类)中都有同名成员,则遵循就近原则。
super 和 this 的比较
| 区别点 | this | super | |
|---|---|---|---|
| 1 | 访问属性 | 访问本类中的属性,如果本类没有此属性,则从父类中继续查找 | 从父类开始查找属性 |
| 2 | 调用方法 | 访问本类中的方法,如果本类没有此方法,则从父类中继续查找 | 从父类开始查找方法 |
| 3 | 调用构造器 | 调用本类构造器,必须放在构造器首行 | 调用父类构造器,必须放在子类构造器首行 |
| 4 | 特殊 | 表示当前对象 | 子类中访问父类对象 |
多态
多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
方法或对象具有多种形态
方法的多态:方法重写和重载就体现多态
对象的多态(核心):
- 一个对象的编译类型和运行类型可以不一致; 如:
Animal animal = new Dog(); - 编译类型在定义对象时,就确定了,不能改变;
- 运行类型是可以变化的,可通过
getClass()查看运行类型; - 编译类型看定义时
=号的左边,运行类型看=号的右边
- 一个对象的编译类型和运行类型可以不一致; 如:
多态的特点
- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
- 多态不能调用“只在子类存在但在父类不存在”的方法;
- 如果子类重写了父类的方法,真正执行的是子类重写的方法,如果子类没有重写父类的方法,执行的是父类的方法。
向上转型
本质:父类的引用指向了子类的对象
语法:父类类型 引用名 = new 子类类型();
向上转型调用方法的规则如下:
- 可以调用父类中的所有成员(遵守访问权限);不能调用子类中的特有成员;
- 在编译期,只能调用父类中声明的方法,但在运行期,实际执行的是子类重写父类的方法;
- 在编译阶段,能调用哪些成员,是由编译类型来决定的(编译器);
- 最终运行效果看子类的具体实现。
示例3
1 | |
向下转型
语法:子类类型 引用名 = (子类类型)父类引用;
- 只能强转父类的引用,不能强转父类的对象;
- 要求父类的引用必须指向的是当前目标类型的对象;
- 当向下转型后,可以调用子类类型中所有的成员。
编译类型和运行类型不一致才需要向下转型:
1 | |
其他细节
- 属性没有重写之说,属性的值看编译类型。(示例4)
示例4
1 | |
instanceof比较操作符:用于判断对象的运行类型,是否为XX类型或者XX类的子类型。(示例5)
示例5
1 | |
动态绑定机制
- 当调用对象方法时,该方法会和该对象的内存地址/运行类型绑定
- 当调用对象属性时,没有动态绑定机制,哪里声明,哪里使用。
示例6
1 | |
多态的应用
多态数组
数组的定义类型为父类类型,里面保存的实际元素类型为子类类型。
示例7
1 | |
多态参数
方法定义的形参类型为父类类型,实参类型允许为子类类型。
三、接口与抽象类
接口
基本介绍
接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。
1 | |
- 默认方法和静态方法是 jdk8 新增,jdk7 以前接口内所有方法均无方法体,即都是抽象方法。
- 接口中的方法会被隐式的指定为
public abstract - 接口中的变量会被隐式的指定为
public static final变量
规则
- 由于自带
static,接口中属性的访问语法:接口名.属性名 - 一个类实现了接口,必须实现接口中所有的方法,并且这些方法只能是
public;(IDEA快捷键:ctrl+i 或 alt shift enter) - 抽象类实现接口,可以不用实现抽象方法;
- 接口支持多继承;(一个接口不能继承其他类,但是可以继承多个别的接口)
- 一个类可以同时实现多个接口;
- 接口的修饰符只能是 public 和 默认,这点和类一样。
为什么需要接口?
- 接口比抽象类还要抽象,可以更加规范地对子类进行约束,全面地实现了:规范和具体实现的分离。
- 接口就是规范,定义的是一组规则。本质是契约。
- 项目的具体需求是多变的,开发要以不变(规范)应万变。因此开发项目往往都是面向接口编程。
实现类可以不必覆写**default**方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
接口的多态性
接口引用可以指向实现了该接口的对象实例。
示例3-1
1 | |
示例3-2
1 | |
示例3-3(多态数组)
1 | |
接口的多态传递
1 | |
标记接口
没有任何方法的接口被称为标记接口。
标记接口是计算机科学中的一种设计思路,用于给那些面向对象的编程语言描述对象。因为编程语言本身并不支持为类维护元数据,而标记接口可以用作描述类的元数据,弥补了这个功能上的缺失。对于实现了标记接口的类,我们就可以在运行时通过反射机制去获取元数据。
以Serializable接口为例,如果一个类实现了这个接口,则表示这个类可以被序列化。因此,我们实际上是通过了Serializable这个接口给该类标记了【可被序列化】的元数据,打上了【可被序列化】的标签。这也是标记/标签接口名字的由来。
在Java中,标记接口主要有以下两种目的:
- 建立一个公共的父接口。比如
EventListener接口,一个由几十个其它接口扩展的Java API,当一个接口继承了EventListener接口,Java虚拟机(JVM)就知道该接口将要被用于一个事件的代理方案。同样的,你可以使用一个标记接口来建立一组接口的父接口。 - 向一个类添加数据类型。这种情况是标记接口最初的目的,实现标记接口的类不需要定义任何接口方法(因为标记接口根本就没有方法),但是该类通过 Java 的多态性可以变成一个接口类型。
更多的,一些容器例如 Ejb 容器,Servlet 容器或运行时环境依赖标记接口识别类是否需要进行某种处理,比如Serialialbe接口标记类需要进行序列化操作。
当然了,在现在 Spring 流行的时代,注解(Annotation)已经成为了最好的维护元数据的方式。因为注解能声明在包、类、字段、方法、局部变量、方法参数等之上,既灵活又方便地起到维护元数据的目的。
Java 8 新增
Java 8 引入的default方法用于提供接口方法的默认实现,可以在实现类中被覆盖。这样就可以在不修改实现类的情况下向现有接口添加新功能,从而增强接口的扩展性和向后兼容性。
1 | |
Java 8 引入的static方法无法在实现类中被覆盖,只能通过接口名直接调用( MyInterface.staticMethod()),类似于类中的静态方法。static方法通常用于定义一些通用的、与接口相关的工具方法,一般很少用。
1 | |
Java 9 允许在接口中使用 private 方法。private方法可以用于在接口内部共享代码,不对外暴露。
1 | |
抽象类
是一种模板模式。抽象类为所有子类提供一个通用模板,子类可以在这个模板的基础上进行扩展。
通过抽象类,可以避免子类设计的随意性。通过抽象类,严格限制子类的设计,使子类之间更加通用。
抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。因此,抽象方法实际上相当于定义了“规范”
注意事项
- 有抽象方法的类只能定义为抽象类,抽象类不一定包含抽象方法;
- 抽象类不能实例化,即不能用
new来实例化抽象类; - 抽象类可以有任何成员(属性、方法、构造方法)【抽象类本质还是类】,但是构造方法不能用来new实例,只能用来被子类调用;
- 抽象类只能用来继承;
- 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非也声明为抽象类。即抽象方法必须被子类实现。
- 抽象方法不能有主体,即不能实现;(不能有{大括号})
- 抽象方法不能用
private、final和static来修饰,因为这些关键字都是和重写相违背。
面向抽象编程
把方法的设计与实现分离
设计:这个类有什么方法、方法的声明(方法名、返回值、形参)
实现:具体的方法体
1 | |
本质是:
- 上层代码只定义规范(例如:abstract class Person);
- 不需要子类就可以实现业务逻辑(正常编译);
- 具体的业务逻辑由不同的子类实现,调用者并不关心。
实践
需求:
- 有多个类,完成不同的任务job
- 要求统计得到各自完成任务的时间
设计一个抽象类(Template),能完成如下功能:
- 编写方法
calculateTime(),可以计算某段代码的耗时时间 - 编写抽象方法
job() - 编写一个子类
Sub,继承抽象类Template,并实现job方法 - 编写一个测试类
TestTemplate进行测试
1 | |
抽象类和接口的异同点
接口和抽象类的共同点
实例化:接口和抽象类都不能直接实例化,只能被实现(接口)或继承(抽象类)后才能创建具体的对象。
抽象方法:接口和抽象类都可以包含抽象方法。抽象方法没有方法体,必须在子类或实现类中实现。
接口和抽象类的区别
设计目的:接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
继承和实现:一个类只能继承一个类(包括抽象类),因为 Java 不支持多继承。但一个类可以实现多个接口,一个接口也可以继承多个其他接口。
成员变量:接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值。抽象类的成员变量可以有任何修饰符(private, protected, public),可以在子类中被重新定义或赋值。
方法:
- Java 8 之前,接口中的方法默认是
public abstract,也就是只能有方法声明。自 Java 8 起,可以在接口中定义 default(默认) 方法和 static (静态)方法。 自 Java 9 起,接口可以包含 private 方法。 - 抽象类可以包含抽象方法和非抽象方法。抽象方法没有方法体,必须在子类中实现。非抽象方法有具体实现,可以直接在抽象类中使用或在子类中重写。
四、类与对象
类与对象关系

属性/成员变量
- 从概念或叫法上看:成员变量 = 属性 = 字段(field)。
- 属性是类的一个组成部分,一般是基本数据类型,也可以是引用类型(对象、数组)。
- 属性的定义语法:访问修饰符 属性类型 属性名 示例:
protected String name;。 - 属性的定义类型可以为任意类型,包括基本类型和引用类型。
- 属性如果没有赋值,有默认值,规则和数组一致:int 0, short 0, byte 0, long 0, float 0.0, double 0.0, char \u0000, boolean false, String null;
对象创建
- 先声明后创建
1 | |
- 对象名 cat 在栈指向一个空的空间
- 在堆开辟空间,同时分配地址(只要有数据空间,就会有地址),把地址赋给 cat 对象名
- 直接创建
1 | |
流程
- 先在方法区加载
Cat类信息(属性和方法信息,只会加载一次); - 在堆中分配空间,进行默认初始化;
- 把地址赋给
whiteCat,whiteCat就指向对象; - 进行指定初始化,比如
whiteCat.name = "小白";whiteCat.age = 6;``whiteCat.color = "white";
内存布局

⚠ JDK1.7 ,字符串常量池和静态变量从永久代(方法区)移动了 Java 堆中
永久代是方法区的具体实现,和堆一样都是由 JVM 管理的运行时数据区域,但堆有更高的 GC 回收效率,因此把需要大量进行字符串回收的字符串常量池移到堆中。
JDK1.8 ,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是本地内存,不受 JVM 内存的限制。
详见:Java内存区域详解
类和对象的内存分配机制
栈:一般存放基本数据类型(局部变量)
堆:存放对象(Cat cat,数组等)
方法区:常量池(常量,比如字符串),类加载信息

对象创建的流程分析

jdk 1.7以前
流程分析:
- 加载类信息(Xxx.class),只会加载一次
- 在堆中分配空间(地址)
- 完成对象初始化(默认初始化、显式初始化、构造器初始化)
- 把对象在堆中的地址返回给对象的引用(对象名)
成员方法
方法调用机制
方法执行会开辟一个独立的栈空间,方法执行完毕该空间就会释放。

同一类中的方法调用:直接调用
跨类中的方法调用:需要先创建对象,通过对象名调用
成员方法的定义
1 | |
访问修饰符:控制方法使用的范围,public、protected、默认(不写)、private
形参列表:表示成员方法输入 ;
- 可以有0或多个参数,用逗号分隔;
- 参数类型可以为任意类型,包括基本和引用类型;
- 调用带参数的方法时,一定要对应着参数列表传入相同类型或兼容类型的参数;
- 方法定义时的参数成为形式参数(形参/虚参);方法调用时传入的参数成为实际参数(实参);实参和形参的类型要一致或兼容,个数、顺序也要一致;
返回数据类型:表示成员方法输出,void 表示没有返回值:
- 一个方法最多有一个返回值;(如果要返回多个值,可以声明数组类型)
- 返回类型可以为任意类型,包括基本和引用类型;
- 如果方法声明了返回数据类型,则方法体中最后的执行语句必须为 return 值;而且要求返回值类型必须和return的值类型一致或兼容
- 如果方法是 void,不能有返回值,方法体中可以没有 return 语句,或者只写 return;
方法体:表示为了实现某一功能代码块;
- 写完成功能的具体语句,可以为输入输出、变量、运算、分支、循环、方法调用等,但不能再定义方法,即方法不能嵌套定义。
return语句不是必须的。
方法传参机制
基本数据类型
AA类中编写一个方法swap,接收两个整数,在方法中交换两个数,主方法中声明两个数,调用swap方法,看两数是否发生变化
1 | |
结论:基本数据类型,传递的是值(值拷贝),形参的任何改变不影响实参

引用数据类型
B类中编写一个方法test100,可以接收一个数组,在方法中修改该数组,看原数组是否变化
1 | |
结论:引用类型,传递的是地址(其实传递也是值,但值是地址),可以通过形参影响实参

this关键字
概念理解
JVM 会给每个对象分配this,代表当前对象。
简单的说,哪个对象调用,this就代表哪个对象。
示例
由于 java 是在虚拟机上跑的,地址是虚拟机的地址,无法直接获取对象的地址。
hashCode()方法会针对不同对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的)
1 | |
输出:

由于this指向当前对象,因此this的地址与当前对象地址相同
内存分析

jdk 1.7以前
可以简单理解成(虽然现实可能不会这样):在对象创建完成后,在堆内隐藏了一个this属性,指向对象本身。
this 用法
this关键字可以用来访问本类的属性、方法、构造器;this用于区分当前类的属性和局部变量;- 访问成员方法的语法:
this.方法名(参数列表); - 访问构造器语法:
this(参数列表);注意只能在构造器中使用(即只能在构造器中访问另一个构造器),并只能放置在第一条语句; this不能在类定义的外部使用,只能在类定义的方法中使用。
五、深拷贝和浅拷贝?什么是引用拷贝?
浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
浅拷贝
浅拷贝的示例代码如下,我们这里实现了Cloneable接口,并重写了clone()方法。
clone()方法的实现很简单,就是直接调用的是父类Object的clone()方法。
1 | |
测试:
1 | |
从输出结构就可以看出, person1的克隆对象和person1使用的仍然是同一个Address 对象。
深拷贝
简单对Person类的clone()方法进行修改,连带着要把Person对象内部的Address 对象一起复制。
1 | |
测试:
1 | |
从输出结构就可以看出,显然person1的克隆对象和person1包含的Address对象已经是不同的了。
什么是引用拷贝?
简单来说,引用拷贝就是两个不同的引用指向同一个对象。

六、static类变量和类方法
6.1 静态变量(类变量)
当我们需要让某个类的所有对象都共享一个变量时,就可以使用。比如:定义学生类,统计所有学生共交多少钱。
6.1.1 基本概念
类变量也叫静态变量/静态属性,是该类所有对象共享的变量。任何一个该类的对象去访问或修改它时,取到的和修改的都是同一个变量。
6.1.2 语法
定义:访问修饰符 static 数据类型 变量名; 【推荐】 或 static 访问修饰符 数据类型 变量名
访问:类名.类变量名 【推荐】或 对象名.类变量名
6.1.3 类变量特点
static变量是同一个类所有对象共享;static变量是随着类的加载而创建,只执行一次,所以没有创建对象实例也可以访问。- 类变量的访问,也必须遵守相关访问权限
- 类变量的生命周期随着类的加载开始,随着类的消亡而销毁。

jdk 1.7以前(from hsp)
2024-8-3更正上图:
方法区会存储已被虚拟机加载的类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。而字符串常量池、静态变量 JDK1.7 开始就从永久代(即方法区)移动到了 Java 堆中。(上图的静态域,按我理解是存储静态变量的区域)

6.2 静态方法(类方法)
类方法也叫静态方法。实际开发往往将一些通用的方法设计成静态方法,这样就不需要创建对象就可以使用,提高开发效率。
6.2.1 语法
定义:访问修饰符 static 数据返回类型 方法名() { }
调用:类名.类方法名 或 对象名.类方法名
特点
静态方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区:
- 静态方法中无this参数;
- 普通方法中隐含this参数
静态方法可以通过类名调用,也可以通过对象名调用
静态方法不中允许使用和对象相关的关键字,比如
this和super。静态方法只能访问静态成员;普通成员方法可以访问静态成员和非静态成员。
6.3 main方法
1)main方法是虚拟机(JVM)调用;
2)JVM 需要调用类得main()方法,所以该方法得访问权限必须是public;
3)JVM 在执行main()方法时不必创建对象,所以该方法必须是static;
4)该方法接收String类型得数组参数,该数组中保存执行 java 命令时传递给所运行的类的参数;
5)命令:java 运行的类名 arg1 arg2 arg3;
6)main方法遵循静态方法规则。

示例6-1:(idea中)
1 | |

七、代码块
7.1 基本概念
- 代码块(Code block)又称为初始化块,属于类中的成员【即是类的一部分】,类似于方法,将逻辑语句封装在方法体中,通过
{}包围起来。 - 和方法不同,代码块没有方法名、返回、参数,只有方法体。
- 不用通过对象或类显示调用,而是加载类的时候,或创建对象时隐式调用。
7.2 基本语法
1 | |
说明:
- 使用
static修饰叫静态代码块,使用Synchronized修饰叫同步代码块; - 分号(
;)可以省略。
7.3 分类
根据其位置和声明的不同,可以分为:
- 局部代码块
- 构造代码块
- 同步代码块
- 静态代码块
7.3.1 局部代码块
在方法中出现,可以限定变量生命周期,及早释放,提高内存利用率。
1 | |
7.3.2 构造代码块
- 在类中方法外出现,每次调用构造方法都会执行,并且在构造方法前执行。
- 相当于另外一种形式的构造器(堆构造器的补充机制),可以做初始化操作。
1 | |
执行结果:
1 | |
因此,构造代码块依赖于构造方法,而且优先于构造方法执行。即实例对象建立,才会运行构造代码块,类不能调用构造代码块。
构造代码块与构造函数的区别:构造代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化。因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。
也就是说,构造代码块中定义的是不同对象共性的初始化内容。如果多个构造器中都有重复的语句,可以抽取到构造初始化块中,提高代码的重用性。
7.3.3 同步代码块
被 Java 中Synchronized关键词修饰的代码块。
Synchronized关键词不仅仅可以用来修饰代码块,与此同时也可以用来修饰方法,是一种线程同步机制,被Synchronized关键词修饰的代码块会被加上内置锁。
作用:在很多场景,我们没有必要去同步整个方法,而只需要同步部分代码即可,也就是使用同步代码块(JDK源码中有很多应用)。
Synchronized同步代码块是一种高开销的操作,因此我们应该尽量减少被同步的内容。
1 | |
此外,静态代码是属于类而不是属于对象的,因此使用Synchronized来修饰静态方法和静态对象的时候,类下的所有对象都会被锁定。
7.3.4 静态代码块
使用static修饰的代码块,在类中方法外出现。
- 随着类的加载而执行,并且只会执行一次。
- 静态块优先于各种代码块以及构造函数。
- 此外静态代码块不能访问普通变量,只能直接调用静态成员。
作用:对类进行初始化。
类什么时候被加载?
- 创建对象实例时(new)
- 创建子类对象实例,父类也会被加载
- 使用类的静态成员时(静态属性、静态方法)
7.4 执行顺序
执行时机
- 静态代码块:在类加载到JVM时初始化,且只被执行一次。
- 构造代码块:在创建实例时,会被隐式的调用。每创建一次(每调用构造方法),构造代码块就会执行一次,构造代码块执行的顺序优先于构造器。
7.4.1 一个类中代码块执行顺序
创建一个对象时,在一个类调用顺序是:
- 执行静态代码块和静态属性的初始化;
- 执行构造代码块和普通属性的初始化;
- 执行构造方法;
代码块和属性初始化执行优先级一样,若有多个则按照代码定义顺序执行。
总结
加载类信息(加载静态代码块和静态属性初始化) -> 创建对象(加载普通代码块和普通属性初始化) -> 调用构造器
7.4.2 继承中代码块执行顺序
- 父类的静态代码块和静态属性(优先级一样,按定义顺序执行);
- 子类的静态代码块和静态属性(优先级一样,按定义顺序执行);
- 父类的构造代码块和普通属性(优先级一样,按定义顺序执行);
- 父类的构造方法
- 子类的构造代码块和普通属性(优先级一样,按定义顺序执行);
- 子类的构造方法
总结
**加载类信息(父→子)**(加载静态代码块和静态属性初始化)-> (父类)加载构造代码块和普通属性初始化 -> (父类)调用构造器 -> (子类)加载构造代码块和普通属性初始化 -> (子类)调用构造器
构造器的最前面隐含了
super()和{普通代码块}。
八、对象初始化详细过程
8.1 一个类及其对象初始化的过程
什么时候需要初始化一个类
首次创建某个对象时:
1 | |
首次访问某个类的静态方法或者静态字段时:
1 | |
Java 解释器就会去找类的路径,定位已经编译好的 Dog.class 文件。
获得类的资源
然后 jvm 就会载入 Dog.class,生成一个class对象。这个时候如果有静态的方法或者变量,静态初始化动作都会被执行。这个时候要注意啦,静态初始化在程序运行过程中只会在 Class 对象首次加载的时候运行一次。这些资源都会放在 jvm 的方法区。
方法区又叫静态区,跟堆一样,被所有的线程共享。
方法区中包含的都是在整个程序中永远唯一的元素,包含所有的 class 和 static 变量。
初始化对象 Dog dog = new Dog()
- 第一次创建
Dog对象先执行上面的一二步 - 在堆上为
Dog对象分配足够的存储空间,所有属性和方法都被设置成默认值(数字为 0,字符为null,布尔为false,而所有引用被设置成null) - 执行构造函数检查是否有父类,如果有父类会先调用父类的构造函数,这里假设
Dog没有父类,执行默认值字段的赋值即方法的初始化动作。 - 执行构造函数。
8.2 有父类情况下的初始化
假设: Dog extends Animal
- 执行第一步,找出
Dog.class文件,接着在加载过程中发现他有一个基类(通过extends关键字),于是先执行Animal类的第一二步,加载Animal类的静态变量和方法,加载结束之后再加载子类Dog的静态变量和方法。
如果Animal类还有父类就以此类推,最终的基类叫做根基类。
因为子类的
static初始化可能会依赖于父类的静态资源,所以要先加载父类的静态资源。
- 接着要
new Dog对象,先为Dog对象分配存储空间 -> 到Dog的构造函数 -> 创建默认的属性。这里其构造函数里面的第一行有个隐含的super(),即父类构造函数,所以这时会跳转到父类Animal的构造函数。
Java 会帮我们完成构造函数的补充,Dog 实际隐式的构造函数如下:
- 父类
Animal执行构造函数前也是分配存储空间 -> 到其构造函数 -> 创建默认的属性 -> 发现已经没有父类了,这个时候就给它的默认的属性赋值和方法的初始化。 - 接着执行构造函数余下的部分,结束后跳转到子类
Dog的构造函数。 - 子类
Dog对默认属性和方法分别进行赋值和初始化,接着完成构造函数接下来的部分。
为什么要执行父类 Animal 的构造方法才继续子类 Dog 的属性及方法赋值?
因为子类 Dog 的非静态变量和方法的初始化有可能使用到其父类 Animal 的属性或方法,所以子类构造默认的属性和方法之后不应该进行赋值,而要跳转到父类的构造方法完成父类对象的构造之后,才来对自己的属性和方法进行初始化。
这也是为什么子类的构造函数显示调用父类构造函数 super() 时要强制写在第一行的原因,程序需要跳转到父类构造函数完成父类对象的构造后才能执行子类构造函数的余下部分。
为什么对属性和方法初始化之后再执行构造函数其他的部分?
因为构造函数中的显式部分有可能使用到对象的属性和方法。
8.3 总结
九、内部类
一个类的内部又完整嵌套了另一个类结构,被嵌套的(里面的)类成为内部类(inner class),嵌套其他类的(外面的)类称为外部类(outer class)。
内部类是类的第五大成员【属性、方法、构造器、代码块、内部类】
特点
内部类最大特点是可以直接访问私有属性,并且可以体现类与类直接的包含关系。
分类
定义在外部类局部位置上(比如方法内):
- 局部内部类(有类名)
- 匿名内部类(没有类名,重点!!!!!!!)
定义在外部类的成员位置上:
- 成员内部类(没用
static修饰) - 静态内部类(有用
static修饰)
局部内部类
定义在外部类的局部位置,比如方法、代码块中,并且有类名,本质仍然是一个类。
重要规则
- 不能添加访问修饰符(由于它地位就是一个局部变量,局部变量不能使用修饰符),但是可以使用final修饰。
- 可以直接访问外部类的所有成员。
- 作用域:仅仅在定义它的方法或代码块中。
- 局部内部类 —> 访问 —> 外部类成员【访问方式:直接访问】
- 外部类 —> 访问 —> 局部内部类成员【访问方式:创建对象再访问】(且必须在作用域内)示例9-1
- 外部其他类 — 不能访问 —> 局部内部类(因为局部内部类地位相当于局部变量)
- 如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,使用:外部类名.this.成员(本质是外部类的对象,即哪个对象调用了m1)
匿名内部类
定义在外部类的局部位置,比如方法、代码块中,并且没有类名,本质仍然是一个类,同时还是一个对象
语法
1 | |
注意:
因为匿名内部类既是一个类的定义,同时也本身也是一个对象,
所以从语法上看,它既有定义类的特征,也有创建对象的特征(对前面代码分析可以看出这个特点),因此可以调用匿名内部类方法。
示例9-1
1 | |
重要规则(与局部内部类相似)
- 不能添加访问修饰符,因为它的地位就是一个局部变量
- 可以直接访问外部类的所有成员,包含私有的
- 作用域:仅仅在定义它的方法或代码块中
- 匿名内部类 —> 访问 —> 外部类成员【访问方式:直接访问】
- 外部其他类 —> 不能访问 —> 匿名内部类
- 如果外部类和匿名内部类的成员重名时,匿名内部类访问的话,默认遵循就近原则,如果想访问外部类的成员,则可以使用(
外部类名.this.成员)访问
成员内部类
定义在外部类的成员位置,并且没有static修饰。
重要规则
可以添加任意访问修饰符
可以直接访问外部类的所有成员,包含私有的
作用域:和外部类的其他成员一样,为整个类体
成员内部类 –> 访问 –> 外部类成员成员【访问方式:直接访问】
外部类 –> 访问 —> 成员内部类【访问方式:创建对象,再访问】
外部其他类 —> 访问 —> 成员内部类
- ① 利用外部类创建内部类对象,再访问;【例:
Outer.Inner inner = outer.new Inner();】(**outer.new Inner();**相当于把new Inner()当作outer对象的成员)
- ① 利用外部类创建内部类对象,再访问;【例:
1 | |
- ② 在外部类中编写一个方法,返回内部类的对象。例:
1 | |
- ③
new Outer().new Inner();相当于①,只是把Outer outer = new Outer();合并了。
- ③
1 | |
- 如果外部类和成员内部类的成员重名时,内部类访问的话,默认遵循就近原则,如果想访问外部类的成员,使用:
**外部类名.this.成员**
静态内部类
定义在外部类的成员位置,有static修饰。
重要规则(与成员内部类相似)
可以添加任意访问修饰符
可以直接访问外部类的所有成员,包含私有的
作用域:和外部类的其他成员一样,为整个类体
静态内部类 –> 访问 –> 外部类成员【访问方式:直接访问所有的静态成员】
外部类 –> 访问 —> 静态内部类【访问方式:创建对象,再访问】
外部其他类 —> 访问 —> 成员内部类
- 方式①:利用外部类创建内部类对象,再访问;【例:
Outer.Inner inner = new Outer.Inner();】**new Outer.Inner();**因为静态内部类是静态成员,可以直接通过类名访问 - 方式②:在外部类中编写一个方法,返回内部类的对象。【例:
public static Inner getInner() { return new Inner(); }``Outer.Inner inner = Outer.getInner();】(非静态也可以)
- 方式①:利用外部类创建内部类对象,再访问;【例:
如果外部类和静态内部类的成员重名时,静态内部类访问的话,默认遵循就近原则,如果想访问外部类的成员,使用:外部类名.成员
1 | |