3.面向对象
1.类和对象的定义
- 1.
类
:指对现实中对象的共性抽取,通常用来表示同一批具有相同特征和行为的对象- 2.
对象
:指类的实例化,其是计算机内存中的一块存储空间,用来存储现实中对象特性和行为的描述- 3.类和对象的关系
- 1.类是对象的
模板
- 2.对象是类的
实例
2.类
- 1.
属性
:用来描述对象的特征
局部变量 成员变量(属性) 位置 形参或方法、代码块内部 类的内部、方法的外部 默认值 没有 有 作用范围 从定义开始到定义的{}结束 当前整个类 命名冲突 局部变量之间不能重名 属性可以和局部重名,局部变量优先更高 - 2.
方法
:用来描述对象行为方法修饰符 返回值类型 方法名(参数列表){ // 操作语句 }
- 3.规则:
- 1.一个源文件中只能有一个
public
类- 2.一个源文件可以有多个非
public
类- 3.源文件的名称应该和
public
类的类名保持一致- 4.如果一个类定义在某个包中,那么
package
语句应该在源文件的首行- 5.如果源文件包含
import
语句,那么应该放在package
语句和类定义之间;如果没有package
语句,那么import
语句应该在源文件中最前面- 6.
import
语句和package
语句对源文件中定义的所有类都有效;同一源文件中不能给不同的类不同的包声明
1.属性
1.类的属性分为
- 1.成员变量
- 1.类变量/静态变量(
static
修饰)- 2.实例变量
类变量 实例变量 修饰符 static关键字 无 访问方式 类或类对象 类的对象 生存周期 静态成员不依赖于类的特定实例,被类的所有实例共享;static 修饰的方法或者变量不需要依赖于对象来进行访问,只要这个类被加载,Java 虚拟机就可以根据类名找到它们 与对象同存亡,被对象调用后才分配内存,调用结束时内存释放 加载时机 运行时Java 虚拟机只为静态变量分配一次内存,在加载类的过程中完成静态变量的内存分配 每创建一个实例,Java 虚拟机就会为实例变量分配一次内存
- 2.局部变量
- 1.函数形参
- 2.方法内定义
- 3.代码块内定义
2.方法
- 1.构造方法
- 2.静态方法
- 3.普通方法
1.方法重载
- 1.
规则
- 1.方法名必须相同,参数列表必须不同(数据类型,个数,顺序)
- 2.跟访问修饰符,返回值,异常没有关系
- 2.
本质
- 1.重载就是同一个类中多个
同名方法
根据不同的传参
来执行不同的逻辑处理
2.方法重写
- 1.
规则
- 1.存在于继承关系
- 2.方法名,参数列表,返回值类型必须与父类保持一致
- 3.访问修饰符必须大于等于父类
- 4.子类不允许抛出比父类更大的异常
- 5.构造方法无法被重写
- 6.子类可以继承并重写父类中的静态方法,但是多态情况下,子类的重写没有意义,因为父类引用执行的静态方法结果永远是父类内容
- 2.
本质
- 1.重写是子类对父类的允许访问的方法的实现过程进行重新编写
- 2.当子类对方法进行重写之后,子类对象调用方法执行的是重写之后的内容
区别点 重载方法 重写方法 发生范围 同一个类 子类中 参数列表 必须修改 一定不能修改 返回类型 可修改 一定不能修改 异常 可修改 可以减少或删除,一定不能抛出新的或者更广的异常 访问修饰符 可修改 一定不能做更严格的限制(可以降低限制) 发生阶段 编译期 运行期
3.this()方法
- 1.用来调用当前类的构造方法
- 2.只能写在
构造方法
有效代码第一行
- 3.由
this()
实参列表决定调用哪个构造方法- 4.构造方法无法通过
this()
调用自身,这样会成为死循环调用,会造成内存溢出- 5.构造方法之间也不能
相互循环调用
,否则会死循环,会造成内存溢出
4.super()方法
- 1.用来调用父类构造方法
- 2.只能写在
构造方法
有效代码第一行
- 3.由
super()
实参列表决定调用哪个父类构造方法- 4.
this()
和super()
不能同时出现- 5.如果
子类构造方法
没有显式调用父类构造,那么无参的super()
默认存在于子类构造方法第一行- 6.当子类构造显式调用
this()
时,当前构造方法就只能使用默认的无参super()
5.构造方法
- 1.
构造方法
是用来创建对象,并且同时给对象的属性赋值- 2.
实例变量
没有手动赋值的时候,系统会赋默认值- 3.
访问修饰符
只能使用public
,且构造方法名和类名
必须一致- 4.
构造方法
不需要指定返回值类型- 5.使用
new
关键字来调用构造方法- 6.当类中没有提供
显式构造方法
,默认提供一个无参数的构造方法- 7.当类中提供了
显式构造方法
,将不再默认提供无参数构造方法- 8.建议将无参数构造方法显示声明,这样一定不会出问题
- 9.构造方法支持
方法重载
,不支持方法重写
6.静态方法和普通方法
- 1.
定义
: 使用static
修饰的方法称为静态方法- 2.
加载时机
:随着类加载
而加载到内存中,非静态方法
属于对象的具体实例,只有在类的对象创建时才会加载入内存中- 3.
静态方法
只能访问静态数据
,非静态方法
既可以访问静态数据
又可以访问非静态数据
- 4.因为
静态方法
和静态数据
会随着类加载而被加载到内存中,而非静态方法
和非静态数据
只有在对象创建时
才会加载到内存中,如果静态方法
调用了了非静态数据
,则在静态方法加载时无法从内存中找到非静态数据
,势必会出错,这种做法是Java虚拟机
不允许的- 5.引用静态方法时,可以用
类名.方法名
或者对象名.方法名
的形式- 7.静态方法中不能使用
this
和super
关键字
7.代码块和静态代码块
class 类名{ static{ // 静态初始代码块 } { // 普通代码块 } }
- 1.
static
修饰的代码块称为静态代码块
- 2.
静态代码块
只有在类加载
时才会执行,而且一个静态代码块
没有执行完成时不会执行下一个静态代码块
,一般将一些只需要进行一次的初始化操作都放在静态代码块中进行- 3.
普通代码块
是在创建对象的时执行,创建几个对象就执行多少次- 4.
执行顺序
:静态代码块
>普通代码块
>构造函数
- 5.
静态代码块
只能访问类中的静态成员变量
- 6.
普通代码块
只能访问类中的成员变量
- 7.
位置
:两者一般都写在属性之下,方法之上,静态代码块类似于一个方法,但它不可以存在于任何方法体中,静态代码块可以置于类中的任何地方,类中可以有多个静态初始化块- 8.
作用
:静态代码块一般是为了给静态成员变量赋值,代码块一般是为了给成员变量赋值- 9.如果类中包含多个静态代码块,则
Java
虚拟机将按它们在类中出现的顺序依次执行它们,每个静态代码块只会被执行一次- 10.静态代码块与静态方法一样,不能直接访问类的实例变量和实例方法,而需要通过
类的实例对象
来访问
8.方法返回值
- 1.指获取到的某个方法体中的
代码执行后产生的结果
- 2.返回值的作用是接收结果,使得其可以用于其他的操作
- 3.方法返回值可以是
基本数据类型
,也可以是引用数据类型
4.类加载机制
- 1.参考
JVM及GC
文章中的类加载子系统
3.对象
1.对象的创建方式
- 1.
new
关键字
- 1.最常用的一种方式,通过
new
关键字调用类的有参或无参构造方法来创建对象Object obj = new Object()
- 2.
反射
,Class
类的newInstance
使用类的public
无参构造,因此必须有public
无参构造才行,内部调用还是Constructor
的newInstance()
方法
- 3.
Constructor.newInstance
,java.lang.relect.Constructor
有一个newInstance
方法可以创建对象,通过这个newInstance
方法调用有参数(不再必须是无参)的和私有的构造函数(不再必须是public
)
- 4.
Clone
方法,必须先实现Cloneable
接口并复写Object
的clone
方法,因为该方法是protected
的,若不复写外部无法调用
- 5.反序列化,当序列化和反序列化一个对象,
JVM
会创建一个单独的对象,在反序列化时,JVM
创建对象并不会调用任何构造函数,类需要实现Serializable
接口
- 6.可参考:
https://cloud.tencent.com/developer/article/1497720
- 7.静态方法只能访问静态成员,不可以访问非静态成员;因为静态方法加载时,优先于对象存在,所以没有办法访问对象中的成员
- 8.静态方法中不能使用
this
,super
关键字;因为this
代表对象,而静态在时有可能没有对象,所以this
无法使用
2.对象的创建过程
- 1.给属性分配空间,赋予默认值
- 2.给属性赋予初始值
- 3.利用构造给属性赋值
3.对象的内存结构
- 1.
对象引用
保存在栈
中,引用保存的是对象的地址(堆地址)
4.this关键字
- 1.
this
代表当前对象- 2.
this.属性名
代表当前对象的属性- 3.
位置
:可以存在于构造方法
和普通方法
内部- 4.当构造方法中实参和形参的变量名
相同
时,可以使用this
关键字指定当前类的属性
5.super关键字
- 1.
super.属性名
:用来指明父类属性- 2.
super.方法名
:用来指明调用父类方法
6.深拷贝和浅拷贝
- 1.
浅拷贝
:对基本数据类型进行值
传递,对引用数据类型进行引用传递
的拷贝- 2.
深拷贝
:对基本数据类型进行值
传递,对引用数据类型创建一个新的对象,并复制其内容
1.浅拷贝
- 1.
Java
中数据类型可以分为两大类:基本类型
和引用类型
- 1.
基本类型
:byte、char、short、int、long、float、double、boolean
- 2.
引用类型
:类、接口、数组、枚举等- 3.浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象,因此原始对象及其副本引用同一个对象,当修改其中一个对象的属性,另一个也会跟着变化
1.实现浅拷贝
- 1.实现浅拷贝需要满足以下条件
- 1.实现
Cloneable
接口,否则会抛出CloneNotSupportedException
异常- 2.重写
clone()
方法,并使用super.clone()
来创建新的对象- 2.注意:
Cloneable
接口是个空接口,super.clone()
调用的是Object
的clone()
方法,Object
类中clone()
是一个本地方法
2.深拷贝
- 深拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,无论该字段是值类型的还是引用类型,都复制独立的一份,当修改其中一个对象的任何内容时,都不会影响另一个对象的内容
1.实现深拷贝
- 1.深拷贝的原理就是要让
原始对象
和克隆对象
所具有的引用类型属性不指向同一块堆内存
,实现方式如下如示
- 1.让每个引用类型属性内部都重写
clone()
方法
- 1.引用类型不能实现深拷贝,那么可以将每个引用类型都拆分为基本类型,分别进行浅拷贝
- 2.
这种方式的缺点
:有多少个引用类型,就要重写多少次,如果存在很多引用类型,那么代码量会很大,所以这种方法不合适package com.ys.test; public class Address implements Cloneable{ private String provices; private String city; public void setAddress(String provices,String city){ this.provices = provices; this.city = city; } @Override public String toString() { return "Address [provices=" + provices + ", city=" + city + "]"; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } } //Person.class 的 clone() 方法 @Override protected Object clone() throws CloneNotSupportedException { Person p = (Person) super.clone(); p.address = (Address) address.clone(); return p; }
- 2.利用序列化和反序列化
- 1.
序列化
是将对象写到流中便于传输,反序列化
则是把对象从流中读取出来- 2.写到流中的对象则是
原始对象的一个拷贝
,因为原始对象还存在JVM
中,所以可以利用对象的序列化
产生克隆对象,然后通过反序列化
获取这个对象- 3.因为序列化产生的是
两个完全独立的对象
,所有无论嵌套多少个引用类型,序列化都是能实现深拷贝的//深度拷贝 public Object deepClone() throws Exception{ // 序列化 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); // 反序列化 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); }
4.抽象类
- 1.用
abstract
修饰的类- 2.抽象类不能
实例化对象
,设计理念是为了服务其他子类,所以规定不能实例化,可参考https://blog.csdn.net/qq_42859864/article/details/103394768
- 3.其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样
- 4.由于抽象类不能实例化对象,所以
抽象类必须被继承才能被使用
- 5.父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法
- 6.
Java
中抽象类表示的是一种继承关系,一个类只能继承一个抽象类,而一个类却可以实现多个接口- 7.
private
,static
,final
不能与abstract
连用,因为abstract
需要被继承实现且无法被实例化
1.抽象方法
- 1.用
abstract
修饰的方法- 2.抽象类中的抽象方法只是声明,不包含方法体
- 3.抽象方法具体实现由它的子类实现
- 4.抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类
- 5.任何
子类必须重写父类的抽象方法
,或者声明自身为抽象类- 6.最终必须有子类实现该抽象方法,否则从最初的父类到最终的子类都不能用来
实例化对象
- 7.构造方法,静态方法(用
static
修饰的方法)不能声明为抽象方法
5.接口
interface 接口名{ } class 类名 implements 接口名{ }
- 1.使用关键字
interface
- 2.类是描述对象的属性和方法,接口则是
包含类要实现的方法
- 3.类通过实现接口的方式,从而来实现接口的抽象方法
- 4.除非实现接口的类是
抽象类
,否则该类要定义接口中的所有方法- 5.
接口无法被实例化
,但是可以被实现,一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类
- 6.接口中可以有属性,且属性都是
公开静态常量
(反编译后可以看到,所以需要赋初始值)- 7.接口中的方法都是
公开抽象方法
(反编译后可以看到,所以没有方法体)- 8.
构造方法不能是抽象的
,所以接口中不存在构造方法,抽象类中存在默认的构造方法- 9.因为接口中内容的修饰符固定,所以一般可以省略不写
- 10.
jdk8
后,接口中可以存在默认方法default 返回值类型 方法名(参数列表){ }
- 11.为了能给接口拓展新功能,而又不必每个子类都要实现此方法,因此
Java8
新加了default
关键字,被其修饰的方法可以不必由子类实现,并且由dafault
修饰的方法在接口中有方法体,这打破了Java
之前对接口方法的规范- 12.如果接口中的方法跟父类中的方法出现冲突,优先执行父类方法(类优先规则)
- 13.如果实现类实现的多个接口中存在方法冲突,则实现类必须对该方法再次进行重写,执行自己重写后的内容
- 14.接口中可以存在
静态方法
static 返回值类型 方法名(参数列表){ }
- 15.
JDK9
接口中可以存在私有方法private 返回值类型 方法名(参数列表){ }
- 16.
Java
接口可以继承多个接口
- 1.
Java
不允许类多继承的主要原因:如果A
同时继承B
和C
,而B
和C
同时有一个D
方法,A
无法决定继承哪一个- 2.接口不存在这样的问题,接口全都是抽象方法继承谁都无所谓,所以接口可以继承多个接口
- 3.一个类如果实现了一个接口,则要实现该接口的所有方法,包括该接口继承的接口的所有方法
- 4.因为接口的方法默认是
public
类型的,所以在实现时一定要用public
来修饰,否则缩小了方法的使用范围
1.接口的分类
- 1.
常量式接口
:接口中只定义了常量,未定义方法- 2.
标记式接口
:接口中未定义任何内容- 3.
函数式接口
:接口中只定义一个方法- 4.
普通接口
:接口中定义了多个方法
2.实现类
class 类名 extends 父类类名 implements 接口名{ }
- 1.必须对接口中所有的方法提供方法实现
- 2.如果实现类不想实现接口中的方法,则其自身只能成为
抽象类
- 3.一个实现类可以同时实现多个接口,一个接口也可以拥有多个实现类
- 4.当实现类实现多个接口时,必须对所有接口中的所有方法都提供方法实现
- 5.实现类在实现接口的同时也可以继承父类
- 6.仍然可以使用多态创建实现类对象,但利用多态创建对象时,引用无法实现类独有内容
- 7.多态中如果想要使用实现类独有内容,则需要进行强制类型转换,实现类之间不能进行强转
3.接口和抽象类的区别
抽象类 接口 关键字 abstract class /extends interface/implements 属性 没有具体要求 公开静态常量 方法 可以存在非抽象的方法 公开抽象方法 构造方法 有构造方法 没有构造方法 继承性 单继承 多继承
- 1.
接口
:自上向下,不需要考虑子类,实现类需要去实现接口的内容- 2.
抽象类
:自下向上,是对子类共性的抽取
6.三大核心思想
1.封装
- 1.保证了程序和数据都不受外部干扰且不被误用
- 2.封装的目的在于保护信息,保护对象中的数据不被外界随意访问
- 3.步骤:
- 1.
属性私有化
private 数据类型 属性名;
- 2.提供取值和赋值的方法(
getter\setter
方法),使外界通过调用方法的形式访问属性
2.继承
- 1.指子类拥有父类的全部特征和行为,关键字
extends
- 2.
Java
只支持单继承- 3.将子类中的共性进行抽取,生成父类,子类在继承关系下可以继承父类所有可被继承的内容
- 4.继承的前提是建立在
is-a
关系上class 子类类名 extends 父类类名{ }
- 5.规则
- 1.子类只能拥有一个父类(
单继承
),但是一个父类可以拥有多个子类- 2.父类的构造方法子类无法继承
- 3.子类在继承父类内容的基础上可以拥有自己独有的内容
- 4.父类无法访问子类独有内容
- 5.子类在是子类的基础上还可以是其他类的父类
- 6.子类可以继承所有直接父类或者间接父类的所有可被继承的内容
- 7.父类的
私有内容
子类不可被继承- 8.父类封装:因为父类也是描述型的类,为了数据安全,也需要封装
- 9.父类封装之后,子类无法直接继承父类私有属性,但是可以通过调用
getter/setter
方法的方式访问
1.对象创建过程(继承关系)
- 1.给父/子类属性分配空间,给父/子类属性赋默认值
- 2.给父类属性赋初始值
- 3.利用父类构造给父类属性再次赋值
- 4.给子类属性赋初始值
- 5.利用子类构造给子类属性再次赋值
- 6.先创建父类对象才能创建子类对象
3.多态
父类引用 引用名 = new 子类类名();
- 1.父类引用可以指向多个
不同的子类对象
- 2.使用
- 1.建立在
继承关系
之上- 2.实际创建的是
子类对象
- 3.方法执行
优先执行子类中重写
的内容- 4.父类引用无法使用
子类独有
的内容- 3.引用类型之间的相互转换
- 1.
自动类型转换
- 1.子类类型赋值给父类类型
父类类名 引用名 = new 子类类名();
- 2.
强制类型转换
- 1.父类引用赋值给子类引用
- 2.转换的子类类型必须跟父类引用指向的子类类型保持一致
- 3.子类之间不能进行类型转换
子类类名 引用名 = (子类类名)父类引用名;
- 4.多态的使用场景
- 1.
数组
:将数组数据类型声明为父类类型,则该数组中可以存放父类对象及其子类对象
- 2.
参数
:将形参声明为父类类型,调用方法时则可以传入父类对象及其子类对象
- 3.
返回值
:将返回值类型声明为父类类型,则该方法可以返回父类对象及其子类对象
- 5.多态的好处
- 1.减少代码冗余
- 2.解耦合
- 3.提升代码可重用性
1.instanceof关键字
- 1.
instanceof
是Java
中的二元运算符
,左边是对象,右边是类- 2.当对象是类及其子类所创建对象时,返回
true
;否则,返回false
- 3.
作用
:判断引用是否与指定类类型兼容,兼容返回true
,不兼容返回false
- 4.语法
引用名 instanceof 类名
- 5.应用场景:通常用于类型强转之前判断是否兼容,否则不能强转
8.内部类
- 1.类的内部再次声明定义类
- 2.
作用
:打破封装,又不破坏封装- 3.分类
- 1.成员内部类
- 2.静态内部类
- 3.局部内部类
- 4.匿名内部类
1.成员内部类
class 外部类名{ class 内部类类名{ } }
- 1.位置
- 1.类的内部,方法的外部,与属性和方法平级
- 2.使用
- 1.成员内部类可以无条件访问外部类的
所有成员属性
和成员方法
(包括private
成员和静态成员)- 2.成员内部类不能定义
静态内容
,但是可以访问外部类静态内容
- 3.如果外部类属性,内部类属性,内部类局部变量出现重名
- 1.外部类属性:
外部类类名.this.属性名
- 2.内部类属性:
this.属性名
- 3.局部变量:
属性名
- 4.内部类对象的创建需要依赖
外部类对象
外部类类名.内部类类名 内部类对象名 = 外部类对象名.new 内部类类名
- 5.注意
- 1.当
成员内部类
拥有和外部类
同名的成员变量或者方法时,会发生隐藏现象
,即默认情况下访问的是成员内部类
的成员,如果要访问外部类的同名成员,需要通过以下形式进行访问
- 1.
外部类.this.成员变量
- 2.
外部类.this.成员方法
- 2.虽然
成员内部类
可以无条件地访问外部类的成员
,但外部类
想访问成员内部类的成员
必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问
2.静态内部类
class 外部类类名{ static class 内部类类名{ } }
- 1.位置
- 1.同成员内部类
- 2.使用
- 1.可以定义
静态内容
,但是无法访问外部类非静态内容
- 3.如果外部类属性,内部类属性,内部类局部变量出现重名
- 1.访问外部类静态属性:外部类类名.属性名
- 2.访问内部类静态属性:外部类类名.内部类类名.属性名
- 4.静态内部类对象创建要依赖于
外部类类名
,可直接通过外部类类名.内部类类名.静态内容
的方式直接访问内部类静态内容
3.局部内部类
class 外部类类名{ 访问修饰符 返回值类型 方法名(参数列表){ class n内部类类名{ } } }
- 1.位置
- 1.定义在外部类方法内部,跟局部变量平级
- 2.使用
- 1.作用范围:从定义行开始,到所在代码块结束
- 2.无法定义
静态内容
- 3.可以访问外部类的
局部变量
,但是该局部变量必须是常量
(jdk7
之前必须用final
显式修饰,jdk8
之后只需要没有再次更改值)- 4.局部内部类对象只能在
所属的外部类方法
中创建
4.匿名内部类
接口名/父类类名 引用名 = new 接口名/父类类名(){ }
- 1.位置
- 1.利用
多态
创建接口实现类/子类对象时- 2.特点
- 1.将类的声明,方法的定义,对象的创建合三为一
- 3.使用
- 1.必须继承一个父类或者实现一个接口
- 2.一个匿名内部类只能创建一个对象,并且该对象只属于语法声明的父类或者接口引用
- 3.一个引用只能拥有一个匿名内部类,一个匿名内部类也只能属性一个引用
- 4.匿名内部类中只能拥有一个默认的无参构造,无法显式声明构造
- 5.匿名内部类无法定义
静态内容
5.lambda表达式
接口名 引用名 = (形参列表)->{//方法体实现部分}
- 1.简化
匿名内部类
,为接口提供实现类对象- 2.使用
- 1.
jdk8
之后只能作用于函数式接口
- 2.小括号中的
形参声明
要严格跟函数式接口
中的形参声明保持一致
- 3.大括号中的内容是对接口方法提供的
方法实现
- 3.简化方式
- 1.形参的数据类型可省略
- 2.如果
形参只有一个
,小括号可省略,注意如果没有参数,()
不可以省略- 3.如果
方法体语句只有一条
,大括号可省略- 4.如果
方法体语句只有一条return返回语句
,大括号和return
都可省略,注意return
和{}
必须同时省略
9.包装类
- 1.基于八大基本数据类型
byte short int long float double char boolean Byte Short Integer Long Float Double Character Boolean - 2.基本数据类型跟包装类型转换
- 1.
基本数据类型转包装类
- 1.利用构造方法
- 2.利用valueOf方法
包装类型 引用名 = new 包装数据类型(基本数据类型); 包装类 引用名 = 包装类型.valueOf(基本数据类型);
- 2.
包装类转基本数据类型
- 1.利用
xxxValue()
方法,xxx
指要转的基本数据类型基本数据类型 变量名=包装类型.xxxValue();
- 3.
JDK5
之后,官方提供了自动封箱及自动拆箱
,基本数据类型跟包装类型可以直接相互赋值,会由编译器完成转换
- 1.自动封箱:基转包
- 2.自动拆箱:包转基
- 3.基本数据类型和
String
转换
- 1.基本数据类型转
String
- 1.字符串拼接
- 2.
valueOf()
String 变量名 = 基本类型+""; String 变量名 = String.valueOf(基本类型);
- 2.
String
转基本数据类型
- 1.
parseXxx()
,Xxx
指要转向的基本数据类型// String中的数据必须是基本数据类型能接受的数据 // 否则会报java.lang.NumberFormatException数据类型转换异常 基本类型 变量名 = 对应包装类型.parseXxx(String类型)
- 4.包装类型和
String
转换
- 1.包装类型转
String
- 1.字符串拼接
- 2.
toString()
string 变量名 = 包装类型+""; String 变量名=包装类型.toString();
- 2.
String
转包装类型
- 1.
parseXxx()
,Xxx
指要转向的基本数据类型// String中的数据必须是基本数据类型能接受的数据 // 否则会报java.lang.NumberFormatException数据类型转换异常 基本类型 变量名 = 对应包装类型.parseXxx(String类型)
10.常用类
1.Object
- 1.所有类的父类
- 2.如果一个类没有显式声明父类,那一定直接继承自
Object
- 3.
Object
中存放所有类都默认拥有的内容,子类可以根据自身需要选择是否对继承过来的方法进行重写- 4.常用方法
- 1.
getClass()
:返回当前引用的实际对象类型- 2.
hashCode()
:获取当前对象的哈希码值- 3.
equals(Object o)
:判断两个对象是否相同,默认比较的是内存地址
- 4.
toString()
:返回对象的详细信息,会在输出引用名时自动调用- 5.
finalize()
:回收垃圾对象
1.finalize()
- 1.
finalize()
方法用于垃圾回收,一般情况下不需要实现- 2.当对象被回收的时候需要释放一些资源;如
socket
链接,在对象初始化时创建,整个生命周期内有效,那么需要实现finalize
方法关闭这个链接- 3.但是当调用
finalize
方法后,并不意味着GC
会立即回收该对象,所以有可能真正调用的时候,对象又不需要回收了,然后到了真正要回收的时候,因为之前调用过一次,这次又不会调用了,产生问题,所以不推荐使用finalize
方法- 4.
垃圾回收机制
:当内存已经满到不足以支持创建新的对象时,虚拟机会自动调用垃圾对象的finalize()
方法对该垃圾对象进行销毁,从而释放内存空间- 5.
垃圾对象的判断标准
:当对象没有引用指向的时候,虚拟机则会该对象为垃圾对象- 6.
手动进行垃圾回收
:代码中手动调用System.gc()
方法可以手动进行强制垃圾对象回收
2.String
3.Arrays
4.Date
整型和Date类之间并不存在直接的对应关系,只是你可以使用int型为分别表示年、月、日、时、分、秒,这样就在两者之间建立了一个对应关系,在作这种转换时,你可以使用Date类构造函数的三种形式:
Date(int year, int month, int date):以int型表示年、月、日
Date(int year, int month, int date, int hrs, int min):以int型表示年、月、日、时、分
Date(int year, int month, int date, int hrs, int min, int sec):以int型表示年、月、日、时、分、秒
在长整型和Date类之间有一个很有趣的对应关系,就是将一个时间表示为距离格林尼治标准时间1970年1月1日0时0分0秒的毫秒数。对于这种对应关系,Date类也有其相应的构造函数:Date(long date)。
获取Date类中的年、月、日、时、分、秒以及星期你可以使用Date类的getYear()、getMonth()、getDate()、getHours()、getMinutes()、getSeconds()、getDay()方法,你也可以将其理解为将Date类转换成int。
而Date类的getTime()方法可以得到我们前面所说的一个时间对应的长整型数,与包装类一样,Date类也有一个toString()方法可以将其转换为String类。
有时我们希望得到Date的特定格式,例如20020324,我们可以使用以下方法,首先在文件开始引入
import java.text.SimpleDateFormat;
java.util.Date date = new java.util.Date();
//如果希望得到YYYYMMDD的格式
SimpleDateFormat sy1=new SimpleDateFormat("yyyyMMdd");
String dateFormat=sy1.format(date);
//如果希望分开得到年,月,日
SimpleDateFormat sy=new SimpleDateFormat("yyyy");
SimpleDateFormat sm=new SimpleDateFormat("MM");
SimpleDateFormat sd=new SimpleDateFormat("dd");
String syear=sy.format(date);
String smon=sm.format(date);
String sday=sd.format(date);```
11.整型常量池
- 1.
Integer
常量池是Java 5
中引入的一个有助于节省内存、提高性能的特性- 2.
Integer
中有个静态内部类IntegerCache
,里面有个常量cache[]
,即Integer
常量池,常量池的大小为一个字节(-128~127)
,当用包装类直接使用区间内的数字时而不new
对象时,会直接从常量池中取出,而不会额外在堆中开辟空间,节省内存空间- 3.所有整数类型的类都有类似的缓存机制
- 1.
ByteCache
用于缓存Byte
对象- 2.
ShortCache
用于缓存Short
对象- 3.
LongCache
用于缓存Long
对象- 4.
Byte
,Short
,Long
的常量池范围默认都是:-128 到 127
;Byte
的所有值都在常量池中,所以用生成的相同值对象都是相等的- 5.
Character
对象也有CharacterCache
缓存 池,范围是0
到127
- 6.所有整型(
Byte,Short,Long
)的比较规律与Integer
是一样的,但只有Integer
可以通过参数改变范围,通过JVM
的启动参数-XX:AutoBoxCacheMax=size
修改
Integer a = 1;
// int 类型在赋值到 Integer 类时,会自动封装,调用 Integer 的 valueOf(int i) 方法。
Integer a = Integer.valueOf(1);
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
当 i >= -128 && i <= 127 时,Integer.valueOf(i) 会将 i 存储在内部类 IntegerCache的static final Integer cache[]里,这一字节的缓存内存地址是静态的,返回值即:
IntegerCache.cache[i + (-IntegerCache.low)]
因此:
Integer a = 1;
Integer b = 1;
a 和 b 的引用都指向同一个对象,即 a == b。
3.泛型
- 1.
Java
泛型(generics
)是JDK 5
中引入的一个特性,,泛型
提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型- 2.泛型的本质是
参数化类型
,即操作的数据类型
被指定为一个参数
1.泛型方法
- 1.该方法在调用时可以接收
不同类型的参数
,根据传递给泛型方法的参数类型
,编译器适当地处理每一个方法调用- 2.
规则
- 1.所有泛型方法声明都有一个
类型参数声明部分
(由尖括号分隔),该类型参数声明部分在方法返回类型之前
- 2.每一个
类型参数声明部分
包含一个或多个类型参数,参数间用逗号隔开,一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符- 3.类型参数能被用来声明
返回值类型
,并且能作为泛型方法得到的实际参数类型的占位符- 4.泛型方法体的声明和其他方法一样,类型参数只能代表
引用类型
,不能是基本数据类型
- 3.
java
中泛型标记符
- 1.
E - Element
(在集合中使用,因为集合中存放的是元素)- 2.
T - Type
(Java 类)- 3.
K - Key
(键)- 4.
V - Value
(值)- 5.
N - Number
(数值类型)- 6.
?
- 表示不确定的java
类型
2.泛型类/接口
- 1.泛型类的声明和非泛型类的声明类似,除了在类名后面添加了
类型参数声明部分
- 2.泛型类的
类型参数声明部分
包含一个或多个类型参数,参数间用逗号隔开- 3.
一个泛型参数
,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符
3.泛型集合
- 1.使用
泛型集合
时注意类型要保持一致
,否则会出现类型转化异常
4.有界泛型
- 1.
有界的泛型
:限制那些被允许传递到一个类型参数的类型种类范围
- 2.其界限分为
上界
和下界
- 1.上界
- 1.关键字
extends
- 2.比如一个
操作数字的方法
可能只希望接受Number
或者Number子类
的实例- 3.声明一个
有界的类型参数
,首先列出类型参数
的名称,后跟extends
关键字,最后紧跟它的上界
- 4.
//1
处会出现错误,因为getUperNumber()
方法中的参数已经限定了参数泛型上限为Number
,所以泛型为String
是不在这个范围之内- 2.下界
- 1.类型通配符
下界
通过形如List<? super Number>
来定义- 2.表示类型只能接受
Number
及其上层父类类型
,如Object
类型的实例
4.枚举(Enum)
package com.wd.entity; public enum Season { SPRING,SUMMER,AUTUMN,WINTER }
package com.wd.test; import com.wd.entity.Season; public class Test01 { public static void main(String[] args){ test(); } public static void test(){ System.out.println("春天:" + Season.SPRING); System.out.println("夏天:" + Season.SUMMER.name()); System.out.println("秋天:" + Season.AUTUMN.ordinal()); System.out.println("冬天:" + Season.WINTER.ordinal()); } }
- 1.枚举类型的关键字:
enum
,引入版本:JDK5
- 2.枚举类本质上是一个
class
隐式继承于Enum
抽象类(该继承关系不显示,可通过反编译显示),且不能继承其他类,因为Java
类不支持多继承,但可以实现接口- 3.枚举类是
final
的,因此无法继承该类- 4.枚举类的构造函数只能是
private
,因为枚举的实现是多例模式
,为了防止在类的外部就可以通过构造器来新建实例- 5.抽象
Enum
类有name
和ordinal
函数,其中name
方法的作用是取得名字,ordinal
方法的作用的是返回每个枚举常量的索引,类似数组索引
- 6.
values()
:返回枚举类中所有的值,valueOf()
:方法返回指定字符串值的枚举常量
- 7.定义的每个枚举值都是该类的一个公共,静态,常量成员,且该成员的类型仍然是当前类
5.注解(Annotation)
- 1.
Java
注解,JDK5
引入的一种注释机制,同Class
和Interface
一样,注解
也属于一种类型- 2.
Java
注解的修饰符是@Interface
,注解可以修饰Java
中的类
、方法
、变量
、参数
和包
等- 3.不同于
Javadoc
,Java注解
可以通过反射
获取注解内容,同时编译期生成.class
文件时注解可以被嵌入到字节码
中- 4.
Java
虚拟机可以保留注解
内容,在运行时可以获取到注解
内容 , 同时支持自定义Java注解
- 5.
元数据
:注解又称为元数据
,即一种描述数据的数据- 6.
元注解
:作用在其他注解上的注解,即一种描述注解的注解,一般用于自定义注解
- 7.
Java SE
提供11 个内置注解
,其中有5 个是基本注解
,自于java.lang
包;有6个是元注解
,来自于java.lang.annotation
包- 8.基本注解包括
- 1.
@Override
- 2.
@Deprecated
- 3.
@SuppressWarnings
- 4.
@SafeVarargs
- 5.
@FunctionalInterface
- 9.元注解包括
- 1.
@Documented
- 2.
@Target
- 3.
@Retention
- 4.
@Inherited
- 5.
@Repeatable
- 6.
@Native
- 10.注解常见作用
- 1.
生成帮助文档
:@see
、@param
和@return
等- 2.
跟踪代码依赖性,实现替代配置文件功能
:Spring 2.5
开始的基于注解配置,作用就是减少配置- 3.
编译时进行格式检查
:@Override
编译时检查是否重写父类方法,如果父类中不存在该方法,编译器便会报错,提示该方法没有重写父类中的方法
1.基本注解
1.@Override
- 1.
@Override
只能修饰方法
且只能用于方法重写
- 2.
@Override
检查该方法是否是重写方法,如果发现其父类或引用的接口中并没有该方法时,会报编译错误
2. @Deprecated
- 1.
@Deprecated
用来注解类
、接口
、成员方法
和成员变量
等,用于表示某个元素(类、方法等)已过时- 2.当其他程序使用已过时的元素时,编译器将会给出
警告
- 3.
Java 9
为@Deprecated
注解增加了2
个属性
- 1.
forRemoval
:该boolean
类型的属性指定该API
将来是否会被删除- 2.
since
:该String
类型的属性指定该API
从哪个版本被标记为过时class Test { // since属性指定从哪个版本开始被标记成过时,forRemoval指定该API将来会被删除 @Deprecated(since = "9", forRemoval = true) public void print() { System.out.println("这里是C语言中文网Java教程!"); } } public class DeprecatedTest { public static void main(String[] args) { // 下面使用info()方法时将会被编译器警告 new Test().print(); } }
3.@SuppressWarining
- 1.
@SuppressWarnings
注解指示被该注解修饰的元素(以及该程素中的所有子元素)取消显示指定的编译器警告
,且会作用于该元素的所有子元素- 2.通常情况下如果程序中使用没有
泛型限制的集合
将会引起编译器警告,为了避免这种编译器警告,可以使用@SuppressWarnings
注解消除这些警告- 3.注解的使用方式有以下三种
- 1.抑制单类型的警告:
@SuppressWarnings("unchecked")
- 2.抑制多类型的警告:
@SuppressWarnings("unchecked","rawtypes")
- 3.抑制所有类型的警告:
@SuppressWarnings("all")
关键字 用途 all 抑制所有警告 boxing 抑制装箱、拆箱操作时候的警告 cast 抑制映射相关的警告 dep-ann 抑制启用注释的警告 deprecation 抑制过期方法警告 fallthrough 抑制在 switch 中缺失 breaks 的警告 finally 抑制 finally 模块没有返回的警告 hiding 抑制相对于隐藏变量的局部变量的警告 incomplete-switch 忽略不完整的 switch 语句 nls 忽略非 nls 格式的字符 null 忽略对 null 的操作 rawtypes 使用 generics 时忽略没有指定相应的类型 restriction 抑制禁止使用劝阻或禁止引用的警告 serial 忽略在 serializable 类中没有声明 serialVersionUID 变量 static-access 抑制不正确的静态访问方式警告 synthetic-access 抑制子类没有按最优方法访问内部类的警告 unchecked 抑制没有进行类型检查操作的警告 unqualified-field-access 抑制没有权限访问的域的警告 unused 抑制没被使用过的代码的警告
4.@SafeVarargs
- 1.
@SafeVarargs
注解忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告- 2.
@SafeVarargs
注解不适用于非 static
或非 final
声明的方法,对于未声明为static
或final
的方法,如果要抑制unchecked
警告,可使用@SuppressWarnings
注解
5.@FunctionalInterface
- 1.
@FunctionalInterface
注解用来指定某个接口必须是函数式接口- 2.
@FunctionalInterface
注解只能修饰接口,不能修饰其它程序元素- 3.
函数式接口
是为Java 8
的Lambda
表达式准备的,Java 8
允许使用Lambda
表达式创建函数式接口
的实例,因此Java 8
专门增加@FunctionalInterface
注解- 4.
@FunctionalInterface
注解的作用只是告诉编译器检查这个接口,保证该接口只能包含一个抽象方法,否则就会编译出错
2.元注解
- 1.
元注解
是负责对其它注解进行说明的注解,自定义注解时可以使用元注解- 2
.Java 5
定义了4
个元注解
- 1.
@Documented
- 2.
@Target
- 3.
@Retention
- 4.
@Inherited
- 3.
Java 8
增加2
个元注解
- 1.
@Repeatable
- 2.
@Native
- 4.这些注解都在
java.lang.annotation
包中
1.@Documented
- 1.
@Documented
是一个标记注解,没有成员变量- 2.
@Documented
标记这些注解是否包含在用户文档中,注解修饰的注解类会被JavaDoc
工具提取成文档- 3.默认情况下
JavaDoc
不包括注解,如果声明注解时指定@Documented
,就会被JavaDoc
工具处理,注解类型信息会被包括在生成的帮助文档中
2.@Target
- 1.
@Target
注解用来指定一个注解的使用范围
,即被@Target
修饰的注解可以用在什么地方- 2.
@Target
注解有一个成员变量value
用来设置范围,value
是java.lang.annotation.ElementType
枚举类型的数组- 3.下表为
ElementType
的枚举常量
名称 说明 TYPE 用于 类、接口(包括注解类型)或 enum 声明 FIELD 用于 字段声明(包括enum常量) METHOD 用于 方法声明 PARAMETER 用于 正式参数声明 CONSTRUCTOR 用于 构造函数声明 LOCAL_VARIABLE 用于 局部变量声明 ANNOTATION_TYPE 用于 注释类型声明 PACKAGE 用于 包声明 TYPE_PARAMETER 用于 类型参数声明 TYPE_USE 用于 类型的使用
3.@Retention
- 1.
@Retention
用于描述注解的生命周期
,即该注解被保留的时间长短- 2.
@Retention
注解中的成员变量value
用来设置保留策略,value
是java.lang.annotation.RetentionPolicy
枚举类型- 3.下表为
RetentionPolicy
的枚举常量
名称 说明 SOURCE 源文件有效 ,编译期丢弃 CLASS 默认状态,字节码文件中有效,注释将由编译器记录在类文件中,运行时丢弃 RUNTIME 运行时有效,注释将由编译器记录在类文件中,并由VM在运行时保留,因此用RUNTIME修饰的注解可以反射式读取 - 3.生命周期长短排序为:
SOURCE
<CLASS
<RUNTIME
- 4.如果需要运行时动态获取注解信息,只能用
RUNTIME
注解;如果需要编译时进行一些预处理操作,可选用CLASS
注解;如果只是做一些检查性的操作,可选用SOURCE
注解
4.@Inherited
- 1.
@Inherited
是一个标记注解,用来指定该注解可以被子类继承- 2.
@Inherited
即如果某个类使用了被@Inherited
修饰的注解,则其子类将自动具有该注解
5.@Repeatable
- 1.注解是
Java 8
增加的,它允许在相同的程序元素中重复注解- 2.
Java 8
版本以前,同一个程序元素前最多只能有一个相同类型的注解,Java 8
开始支持@Repeatable
,标识某注解可以在同一个声明上使用多次
6.@Native
- 1.@
Native
注解修饰成员变量,则表示这个变量可以被本地代码引用
3.自定义注解
- 1.声明自定义注解使用
@interface
关键字,默认情况下,注解可以在程序的任何地方使用,通常用于修饰类、接口、方法和变量等// 定义一个简单的注解类型 public @interface Test { }
- 2.定义注解和定义类相似,注解前面的
访问修饰符
和类一样有两种
- 1.公有访问权限(
public
)- 2.默认访问权限(
default
,默认不写)- 3.不包含
任何成员变量
的注解称为标记注解
,上面声明的Test
注解以及基本注解中的@Override
注解都属于标记注解- 4.根据需要注解中可以定义
成员变量
,成员变量以无形参的方法
形式来声明,其方法名和返回值定义了该成员变量的名字和类型- 5.如果没有在声明时
指定成员变量的默认值
则在使用时需要手动赋值
,否则会报错public @interface MyTag { // 定义带两个成员变量的注解 // 注解中的成员变量以方法的形式来定义 String name(); int age(); }
public class Test { // 使用带成员变量的注解时,需要为成员变量赋值 @MyTag(name="xx", age=6) public void info() { ... } ... }
- 6.成员变量也可以有
访问权限修饰符
,但是只能有公有权限
和默认权限(不显式声明)
- 7.注解中的
成员变量
也可以有默认值,可使用default
关键字指定默认值public @interface MyTag { // 定义了两个成员变量的注解 // 使用default为两个成员变量指定初始值 String name() default "java"; int age() default 1; }
- 8.如果为注解的成员变量指定了默认值,那么使用该注解时可以不为这些成员变量赋值,而是直接使用默认值
public class Test { // 使用带成员变量的注解 // MyTag注释的成员变量有默认值,所以可以不为它的成员变量赋值 @MyTag public void info() { ... } ... }
- 9.可以在使用注解时为成员变量指定值,如果为注解的成员变量指定了值,则
默认值不会起作用
- 10.根据注解是否包含
成员变量
分为以下两类
- 1.
标记注解
:没有定义成员变量的注解类型被称为标记注解,这种注解仅利用自身的存在与否来提供信息- 2.
元数据注解
:包含成员变量的注解,因为可以接受更多的元数据,所以也被称为元数据注解- 11.注解的使用
基于反射
,通过反射可以去获取注解中的信息
1.实例
6.反射
1.反射的定义
- 1.Java反射是指:在运行状态中,对于任意一个类,能够知道该类的所有属性和方法等信息,对于任意一个对象,能够调用该对象的任意方法和属性等信息,并且能改变它的属性
2.反射的作用
- 1.反射机制是
Java
被视为准动态语言的一个关键性质(该机制允许程序在运行时
通过反射取得任何一个已知名称的class
的内部信息(运行时类的属性,方法,构造,访问修饰符,注解等),并可于运行时改变内容或调用方法
2.反射的优缺点
- 1.优点
- 1.
反射机制
允许程序在运行时取得任何一个已知名称的class
的内部信息,包括包括其modifiers
(修饰符),fields
(属性),methods
(方法)等,并可于运行时改变fields
内容或调用methods
- 2.可以
动态的创建和使用对象
(框架底层核心),使用灵活,没有反射机制,框架技术就失去底层支持- 2.缺点
- 1.使用反射基本是
解释执行
,对执行速度有影响- 2.使用反射可以打破封装性,导致java对象的属性不安全(可以修改对象的私有属性)
3.反射的使用步骤
- 1.使用反射需要首先需要获取
Class
类- 2.构造该类的
实例对象
- 3.通过实例对象获取到需要的
成员变量和方法
等信息或直接通过Class
对象调用内部信息
- 说明:
- 1.获得
Class
有三种方式
- 1.通过
Class
对象的forName()
静态方法,可能抛出ClassNotFoundException
异常- 2.通过对象调用
getClass()
方法- 3.通过
类名.class
方式,该方法最为安全可靠,程序性能更高,同时说明任何一个类都有一个隐含的静态成员变量class
- 2.一个类在
JVM
中有且只有
一个Class
实例- 3.
setAccessible(true)
启用和禁用访问安全检查的开关,true
表示反射的对象在使用时应该取消java
语言的访问检查,反之不取消;如果不取消直接改变类的私有内容
会报错- 4.反射只能获取当前类声明的方法,无法获取父类中声明的方法,直接通过反射获取子类的对象是不能获取父类的属性值的,必须根据反射获得的子类Class对象调用 getSuperclass()方法获取父类对象,然后再通过父类对象去获取父类的属性值
7.序列化和反序列化
1.序列化
1.序列化的定义
- 1.
序列化
:指把内存中的Java对象
通过某种方式存储到磁盘文件
或传递给其他网络节点
(网络上传输)- 2.即将
数据结构或对象
转换成二进制
的过程- 3.如果想一次序列化多个对象可以将对象放到集合当中然后序列化集合,参与序列化的集合以及集合中的元素都需要实现
java.io.Serializable
接口
2.序列化的意义
- 1.
分布式系统
:一般需要将Java对象
通过网络在不同的节点间传输,此时需要将共享的对象数据
转换为二进制形式
,即序列化- 2.
服务器钝化
- 1.如果服务器发现某些对象很久不使用,那么服务器就会把这些内存中的对象以
二进制
的形式持久化
在本地磁盘文件中(Java对象
转换为二进制文件
)- 2.如果服务器发现某些对象需要使用时,先去内存中寻找,找不到再去磁盘文件中
反序列化对象数据
,将二进制数据
恢复成Java 对象
,这样能节省服务器内存
3.序列化的步驟
- 1.需要序列化的类必须实现序列化接口
Java.lang.Serializable
,这是一个标志接口
,没有任何抽象方法- 2.
JVM
底层会判断,如果当前对象是Serializable
接口的实例才允许序列化,通过Java对象 instanceof Serializable
判断- 3.
Java
中使用对象流
来完成序列化
和反序列化
,具体IO流
操作可参考IO/NIO
文章中的对象流实现序列化和反序列化
2.反序列化
1.反序列化的定义
- 1.
反序列化
:指把磁盘文件
中或网络节点
上的对象数据,恢复成Java对象
模型的过程- 2.即将在序列化过程中所生成的
二进制
转换成数据结构或对象
的过程
3.序列化和反序列化的问题
1.transient问题
- 1.某些数据不需要做序列化,只需字段面前加上
transient
private String name;//需要序列化 transient private int age;//不需要序列化
- 2.此时
反序列化
结果则显示name
的具体值,而age
因为没有序列化
则无法传递数据,因此显示整型数据默认值0
2.序列化版本问题
- 1.完成
序列化操作
后由于项目的升级或修改,可能会对序列化对象进行修改
,那么进行反序列化可能会报错
- 2.只需要在类上增加一个
serialVersionUID
字段,用来固定版本
,无论怎么修改,只要版本都是一致的,就能进行反序列化- 3.注意
- 1.
Java
虚拟机针对Serializable
接口会自动生成一个序列化版本号
- 2.这种自动生成的序列化版本号使代码一旦确定之后,就不能进行
后续的修改
- 3.因为只要修改必然会重新编译,此时会生成
全新的序列化版本号
,此时Java
虚拟机会认为这是一个全新的类- 4.因此凡是类实现了
Serializable
接口,建议给该类手动提供一个固定不变的序列化版本号
- 5.这样类即使修改了代码,但是版本号不变,
Java
虚拟机仍会认为是同一个类,可以正常进行反序列化private static final long serialVersionUID = 8656128222714547171L;
8.Java各版本特性
1.Java8
- 1.
Lambda
表达式- 2.函数式接口
- 3.方法引用
- 4.默认方法
- 5.
Stream API
- 6.
Optional
类- 7.
Date Time API
- 8.
Nashorn、JavaScript
引擎
1.Lambda表达式
- 1.
定义
:Lambda
表达式是对匿名函数
的简写形式
,Lambda
允许把函数作为一个方法的参数,可以写出更简洁的代码- 2.
实质
:语法糖
,实质底层还是一个方法
package domains; import lombok.AllArgsConstructor; import lombok.Data; import lombok.Getter; import lombok.NoArgsConstructor; /** * 员工 */ @Data @AllArgsConstructor @NoArgsConstructor @Getter public class Employee { private String name; private int age; private double salary; }
package dao; public interface TestPredicate<T> { boolean test(T e); }
package dao.daoImpl; import dao.TestPredicate; import domains.Employee; public class FilterEmployeeByAge implements TestPredicate<Employee> { @Override public boolean test(Employee employee) { return employee.getAge() > 24; } }
package dao.daoImpl; import dao.TestPredicate; import domains.Employee; public class FilterEmployeeBySalary implements TestPredicate<Employee> { @Override public boolean test(Employee employee) { return employee.getSalary() > 8000; } }
import dao.TestPredicate; import dao.daoImpl.FilterEmployeeByAge; import dao.daoImpl.FilterEmployeeBySalary; import domains.Employee; import org.junit.Test; import java.util.*; public class TestLambda { List<Employee> employees = Arrays.asList( new Employee("张三",8,100), new Employee("李四",12,500), new Employee("王五",18,3000), new Employee("赵六",24,8000), new Employee("田七",35,15000), new Employee("吴八",40,25000) ); /** * 原始 */ @Test public void test1(){ //比较器 - 匿名内部类 Comparator<Integer> comparator = new Comparator<Integer>(){ @Override public int compare(Integer o1, Integer o2) { return Integer.compare(o1,o2); } }; //注意:比较器的类型需要和TreeSet类型一致 TreeSet<Integer> treeSet = new TreeSet<>(comparator); } /** * Lambda表达式化简 */ @Test public void test2(){ //化简1 Comparator<Integer> comparator = (x,y) -> Integer.compare(x,y); TreeSet<Integer> treeSet = new TreeSet<>(comparator); //化简2 TreeSet<Integer> integerTreeSet = new TreeSet<>((x,y) -> Integer.compare(x,y)); } /** * 需求1:获取员工年龄大于24岁的员工信息 */ @Test public void test3(){ List<Employee> filterEmployees = filterAgeEmployees(employees); for (Employee filterEmployee : filterEmployees) { System.out.println(filterEmployee); /* 结果: Employee(name=田七, age=35, salary=15000.0) Employee(name=吴八, age=40, salary=25000.0) */ } } /** * 需求1:获取员工年龄大于24岁的员工信息 优化1 */ @Test public void test4(){ List<Employee> filterEmployees = filterEmployee(employees, new FilterEmployeeByAge()); for (Employee filterEmployee : filterEmployees) { System.out.println(filterEmployee); /* 结果: Employee(name=田七, age=35, salary=15000.0) Employee(name=吴八, age=40, salary=25000.0) */ } } /** * 需求2:获取员工工资大于8000的员工信息 */ @Test public void test5(){ List<Employee> filterEmployees = filterSalaryEmployees(employees); for (Employee filterEmployee : filterEmployees) { System.out.println(filterEmployee); /* 结果: Employee(name=田七, age=35, salary=15000.0) Employee(name=吴八, age=40, salary=25000.0) */ } } /** * 需求2:获取员工工资大于8000的员工信息 优化1 */ @Test public void test6(){ List<Employee> filterEmployees = filterEmployee(employees,new FilterEmployeeBySalary()); for (Employee filterEmployee : filterEmployees) { System.out.println(filterEmployee); /* 结果: Employee(name=田七, age=35, salary=15000.0) Employee(name=吴八, age=40, salary=25000.0) */ } } /** * 根据年龄条件过滤员工信息 * @param employees * @return */ public List<Employee> filterAgeEmployees(List<Employee> employees){ ArrayList<Employee> employeeArrayList = new ArrayList<>(); for (Employee employee : employees) { if(employee.getAge() > 24){ employeeArrayList.add(employee); } } return employeeArrayList; } /** * 根据薪水条件过滤员工信息 * @param employees * @return */ public List<Employee> filterSalaryEmployees(List<Employee> employees){ ArrayList<Employee> employeeArrayList = new ArrayList<>(); for (Employee employee : employees) { if(employee.getSalary() > 8000){ employeeArrayList.add(employee); } } return employeeArrayList; } //优化方式一:策略设计模式. 优点:可以根据传入的策略执行不同的策略 //注意:泛型类型需要填写,否则lambda会发生类型不匹配异常 public List<Employee> filterEmployee(List<Employee> list, TestPredicate<Employee> predicate){ ArrayList<Employee> employeeArrayList = new ArrayList<>(); for (Employee employee : employees) { if(predicate.test(employee)){ employeeArrayList.add(employee); } } return employeeArrayList; } //优化方式二:匿名内部类 @Test public void test7(){ List<Employee> filterEmployees = filterEmployee(employees,new TestPredicate<Employee>(){ @Override public boolean test(Employee employee) { return employee.getSalary() < 5000; } }); for (Employee filterEmployee : filterEmployees) { System.out.println(filterEmployee); /* 结果: Employee(name=张三, age=8, salary=100.0) Employee(name=李四, age=12, salary=500.0) Employee(name=王五, age=18, salary=3000.0) */ } } //优化方式三:Lambda表达式 @Test public void test8(){ List<Employee> filterEmployees = filterEmployee(employees,(e) -> e.getSalary() < 5000); filterEmployees.forEach(System.out::println); } //优化方式四:Stream API @Test public void test9(){ employees.stream().filter((e) -> e.getSalary() < 5000).forEach(System.out::println); } }
1.Lambda基础语法
import org.junit.Test; import java.util.Comparator; import java.util.function.Consumer; /** * Lambda表达式的基础语法:Java8中引入了一个新的操作符->,该操作符为箭> 头操作符或Lambda操作符 * 箭头操作符将Lambda表达式拆分成两部分: * 左侧:Lambda表达式的参数列表 * 右侧:Lambda表达式中所需执行的功能,即Lambda体 * * 语法格式一:无参数,无返回值(小括号不可省略) * () -> System.out.println("Hello Lambda"); * * 语法格式二:一个参数,无返回值(小括号可省略,建议加上) * x -> System.out.println(X); * * 语法格式三:两个及两个以上参数,并且Lambda体中有多条语句,小括号和大括号和return都不能省略 * * 语法格式四:如果Lambda体中只有一条语句,return和大括号皆可省略 * * 语法格式五:Lambda表达式的参数列表的数据类型可以省略不写,因为JVM编辑器通过上下文可以推断出数据类型,即类型推断 * * 总结: * 1.可选类型声明:不需要声明参数类型,编译器可以通过上下文可以推断出数据类型 * 2.可选参数圆括号:一个参数无需定义圆括号,但多个参数或无参需要定义圆括号 * 3.可选的大括号:如果主题体只包含了一个语句,就不需要使用大括号,否则需要大括号 * 4.可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,否则需要加return * * Lambda需要函数式接口的支持 * * 函数式接口:接口中只有一个抽象方法的接口 * * 通过注解@FuncationalInterface修饰可以检查是否为函数式接口 * * 注意事项: * 1.lambda表达式只能引用标记了final的外层局部变量,即不能在Lambda内部修改定义在域外的局部变量,否则会编译错误(jdk7) * 2.lambda表达式的引用的外层局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义,jdk8) * 3.lambda达式当中不允许声明一个与局部变量同名的参数或者局部变量 */ public class TestLambda2 { @Test public void test1(){ //传统匿名内部类 Runnable runnable = new Runnable(){ @Override public void run() { System.out.println("Hello World"); } }; runnable.run(); //lambda表达式 Runnable lambda = () -> System.out.println("Hello Lambda"); lambda.run(); } @Test public void test2(){ Consumer<String> consumer = x -> System.out.println(x); consumer.accept("测试成功!"); } @Test public void test3(){ Comparator<Integer> comparator = (x, y) -> { System.out.println("测试成功!"); return Integer.compare(x, y); }; } @Test public void test4(){ Comparator<Integer> comparator = (x, y) -> Integer.compare(x, y); } }
2.Lambda上下文推断
//不能拆分开写,否则无法根据上下文推断出aaa,bbb,ccc的类型 String[] strs = {"aaa","bbb","ccc"} //后面的泛型可以不写,可以通过前面的泛型推断出来 List<String> list = new ArrayList<>(); //java8中show(new HashMap<>())可以不写泛型,通过上下文类型推断出结果 show(new HashMap<>()); public void show(Map<String, Integer> map){ ... }
2.函数式接口
- 1.定义: 只包含一个
抽象方法
的接口- 2.使用
@FunctionalInterface
注解,可以检查是否为函数式接口
1.四大核心函数式接口
函数式接口 参数类型 返回类型 用途 Consumer<T> 消费型接口 T void 对类型为T的对象应用操作。包含方法:void accept(T t) Supplier<T> 供给型接口 无 T 返回类型为T的对象。包含方法:T get() Function<T, R> 函数型接口 T R 对类型为T的对象应用操作,并返回R类型对象的结果。包含方法:R apply(T t) Predicate<T> 断言型接口 T boolean 确定类型为T的对象是否满足某约束。包含方法:boolean test(T t) import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; /** * Java8内置的四大核心函数式接口 * * Consumer<T></消费型接口> * void accept(T t); * * Supplier<T></供给型接口> * T get(); * * Function<T, R></函数型接口> * R apply(T t); * * Predicate<T></断言型接口> * boolean test(T t); */ public class TestLambda4 { //Consumer<T></消费型接口> @Test public void test1(){ shopping(1000,(m) -> System.out.println("书包,消费" + m/4)); } public void shopping(double money, Consumer<Double> consumer){ consumer.accept(money); } //Supplier<T></供给型接口> @Test public void test2(){ List<Integer> integers = getIntegers(10, () -> (int) (Math.random() * 100)); for (Integer integer : integers) { System.out.println(integer); } } public List<Integer> getIntegers(int num, Supplier<Integer> supplier){ List<Integer> list = new ArrayList<>(); for (int i = 0; i < num; i++) { Integer n = supplier.get(); list.add(n); } return list; } //Function<T, R></函数型接口> @Test public void test3(){ String s = strHandler("三生三世:十里桃花", (str) -> str.replace(":", "+")); System.out.println(s); } public String strHandler(String str, Function<String,String> function){ return function.apply(str); } //Predicate<T></断言型接口> @Test public void test4(){ List<String> statement = Arrays.asList("三生三世,十里桃花","人生若只如初见","三生三世,情非得已"); List<String> stringList = filterStr(statement, (str) -> str.contains("三生三世")); for (String s : stringList) { System.out.println(s); } } public List<String> filterStr(List<String> list, Predicate<String> predicate){ List<String> stringList = new ArrayList<>(); for (String s : list) { if(predicate.test(s)){ stringList.add(s); } } return stringList; } }
2.其他函数式接口
函数式接口 参数类型 返回类型 用途 BiConsumer<T,U> T,U void 对类型为T,U的对象应用操作。包含方法:void accept(T t,U u) BiFunction<T,U,R> T,U R 对类型为T,U参数的对象应用操作,返回R类型的结果。包含方法:R apply(T t,U u) BiPredicate<T,U> T,U boolean 确定类型为T,U的对象是否满足某约束。包含方法:boolean test(T t, U u)
3.引用
- 1.
Lambda
表达式的另一种写法- 2.当要传递给
Lambda
体的操作已经有实现则可以使用方法引用
1.方法引用
import domains.Employee; import org.junit.Test; import java.util.Comparator; import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; /** * 方法引用:若Lambda体中的方法已经被其他类实现了,可以使用方法引用 * * 三种法语格式: * 1.对象::实例方法名 * 2.类::静态方法名 * 3.类::实例方法名(需满足条件2) * * 注意: * 1.Lambda体中调用的方法需要与函数式接口的参数类型和返回值保持一致 * 2.Lambda参数列表的第一个参数是实例方法的调用者,第二个参数是实例方法的参数时,可以使用语法格式三 * */ public class TestLambda5 { //1.对象::实例方法名 @Test public void test1(){ Consumer<String> consumer = (x) -> System.out.println(x); //等价于 //注意:调用的方法需要与函数式接口接口的参数类型和返回值需要一致 Consumer<String> consumer1 = System.out::println; consumer1.accept("200"); shopping(1000, System.out::println); } public void shopping(double money, Consumer<Double> consumer){ consumer.accept(money); } //2.类::静态方法名 @Test public void test2(){ Comparator<Integer> comparator = (x, y) -> Integer.compare(x,y); //等价于 Comparator<Integer> comparator1 = Integer::compareTo; } //3.类::实例方法名 @Test public void test3(){ BiPredicate<String, String> biPredicate = (x, y) -> x.equals(x); BiPredicate<String, String> biPredicate1 = String::equals; } }
2.构造器引用
/** * 构造器引用:若Lambda体中存在新建对象,可以使用构造器引用 * 法语格式: * ClassName::new * 注意: * 1.函数式接口的方法的参数列表匹配对应的实体类的构造器 */ //构造器引用 @Test public void test4(){ Supplier<Employee> supplier = () -> new Employee(); supplier.get(); //等价于 //函数式接口的方法的参数列表匹配对应的实体类的构造器 Supplier<Employee> supplier1 = Employee::new; Employee employee = supplier1.get(); System.out.println(employee); //Function<Integer, Employee> function = (x) -> new Employee(x); Function<Integer, Employee> function = Employee::new; Employee apply = function.apply(1); System.out.println(apply); }
3.数组引用
/* * 数组引用: * 语法格式: * Type[]::new */ //数组引用 @Test public void test5(){ Function<Integer, String[]> function = (x) -> new String[x]; String[] apply = function.apply(10); System.out.println(apply.length); //等价于 Function<Integer, String[]> function1 = String[]::new; String[] apply1 = function1.apply(20); System.out.println(apply1.length); }
5.Stream
1.Stream的概念
- Stream是Java8处理集合的关键抽象概念,它可以指定对集合进行的操作,可以执行非常复杂的查找,过滤,映射数据等操作
- 流(Stream)是数据通道,用于操作数据源(集合,数组等)所生成的元素序列
- 完整流包含以下结构:
- 1.数据源(集合,数组等)
- 2.通道(做一系列流水线式的中间操作)
- 3.新流(终止操作产生)
- 注意:
- 1.Stream不会存储元素
- 2.Stream不会改变数据源,而是返回一个持有结果的新Stream
- 3.Stream操作是延迟执行的,即等到需要结果(终止操作)的时候才会被执行
2.Stream的操作三个步骤
- 1.创建Stream
- 通过一个数据源(如:集合,数组),获取一个流
- 2.中间操作
- Stream的一个中间操作链,对数据源的数据进行处理
- 3.终止操作
- Stream的一个终止操作,执行中间操作链,并产生结果
3.创建Stream的四种方式
import domains.Employee; import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.stream.Stream; /** * Stream的三个操作步骤: * 1.创建Stream * 2.中间操作 * 3.终止操作 */ public class TestLambda6 { //创建Stream @Test public void test1(){ //1.通过Collection系列集合可以获取的stream()串行流或parallelStream() List<String> list = new ArrayList<>(); Stream<String> stream = list.stream(); //2.通过Arrays中的静态方法Stream可以获取数组流 Employee[] employees = new Employee[10]; Stream<Employee> stream1 = Arrays.stream(employees); //3.通过Stream类中的静态方法of() Stream<String> stream3 = Stream.of("aa", "bb", "cc"); //4.创建无限流 //第一种无限流:迭代 Stream<Integer> stream4 = Stream.iterate(0, (x) -> x + 2); //中间操作 Stream<Integer> limit = stream4.limit(10); //终止操作 limit.forEach(System.out::println); //第二种无限流:生成 Stream.generate(() -> new Random().nextInt(100)).limit(10).forEach(System.out::println); } }
4.中间操作
- 延迟加载:中间操作默认不会执行任何操作,只有执行了终止操作,所有的中间操作才会执行,这个过程叫做惰性求值或者延迟加载(多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理,而在终止操作时一次性全部处理)
- 内部迭代:迭代操作由Stream API完成
- 外部迭代:手动完成
1.筛选与切片
List<Employee> employees = Arrays.asList( new Employee("张三",8,100), new Employee("李四",12,500), new Employee("王五",18,3000), new Employee("赵六",24,8000), new Employee("赵九",24,6000), new Employee("田七",35,15000), new Employee("吴八",40,25000), new Employee("吴八",40,25000) ); @Test public void test2(){ /** * 筛选与切片 * Stream<T> filter(Predicate<? super T> predicate):接收一个断言型的Lambda表达式,从Stream流中排除满足条件的某些元素 * Stream<T> limit(long maxSize):截断流,使其元素不超过指定数量;断路:如果发现了满足条件的数据就不再迭代 * Stream<T> skip(long n):调过元素,返回一个跳过了前n个元素的流,若流中的元素不足n个,则返回一个空流(与limit(n)互补) * Stream<T> distinct():筛选,通过流所生成元素的hashCode()和equals()去除重复元素;注意需要重新写hashCode和equals()方法 */ //注意:没有终止操作,中间操作不会执行任何操作 //外层迭代:手动完成 // Iterator<Employee> iterator = employees.iterator(); // while (iterator.hasNext()) { // System.out.println(iterator.next()); // } //内层迭代:迭代操作由Stream API employees.stream().filter((e) -> e.getAge() > 24).forEach(System.out::println); employees.stream().filter((e) -> e.getAge() > 24).limit(1).forEach(System.out::println); employees.stream().filter((e) -> e.getAge() > 24).skip(1).forEach(System.out::println); employees.stream().filter((e) -> e.getAge() > 24).distinct().forEach(System.out::println); }
2.映射
@Test public void test3(){ /** * 映射 * map:接收一个函数型的Lambda表达式,该参数会被应用到每个元素上,并将其映射成一个新的元素,用于将元素转换成其他形式或者提取信息。(把流加入其中) * flatMap:接收一个函数型的Lambda表达式,将流中的每个元素都转换成另一个流,然后把所有流连接成一个流(把一个个流中的元素加入其中) */ List<String> list = Arrays.asList("aaa","bbb","ccc"); list.stream() .map((str) -> str.toUpperCase()) .forEach(System.out::println); employees.stream().map(Employee::getName).forEach(System.out::println); //map与flatMap的区别 Stream<Stream<Character>> streamStream = list.stream().map(TestLambda6::filterCharacter); streamStream.forEach((stream) -> { stream.forEach(System.out::println); }); Stream<Character> characterStream = list.stream().flatMap(TestLambda6::filterCharacter); characterStream.forEach(System.out::println); //类似list的add和addAll List<String> list1 = Arrays.asList("aaa", "bbb", "ccc"); List list2 = new ArrayList<>(); list2.add("111"); list2.add("222"); list2.add("333"); list2.add(list1); /** * 结果 * [111, 222, 333, [aaa, bbb, ccc]] */ list2.addAll(list1); /** * 结果 * [111, 222, 333, [aaa, bbb, ccc], aaa, bbb, ccc] */ System.out.println(list2); } public static Stream<Character> filterCharacter(String str){ List<Character> list = new ArrayList<>(); for (Character c : str.toCharArray()) { list.add(c); } return list.stream(); }
3.排序
@Test public void test4(){ /** * 排序 * sorted():自然排序(Comparable) * sorted(Comparator c):定制排序(Comparator) */ List<String> list = Arrays.asList("ccc", "aaa", "bbb"); list.stream() .sorted() .forEach(System.out::println); employees.stream().sorted((e1 ,e2) -> { if(e1.getAge() == e2.getAge()){ return e1.getName().compareTo(e2.getName()); }else { return -e1.getAge() - e2.getAge(); } }).forEach(System.out::println); }
5.终止操作
- 终止操作会从流的流水线生成结果
1.查找与匹配
@Test public void test5(){ /** * 终止操作 * 1.查找与匹配 * allMatch(Predicate<? super T> predicate):接收一个断言型的Lambda表达式,检查是否流中的所有元素都满足条件 * anyMatch(Predicate<? super T> predicate):接收一个断言型的Lambda表达式,检查流中是否至少有一个元素满足条件 * noneMatch(Predicate<? super T> predicate):接收一个断言型的Lambda表达式,检查是否流中的所有元素都不满足条件 * findFirst:返回第一个元素 * findAny:返回任意元素 * count:返回流中元素的总个数 * max:返回流中最大值 * min:返回流中最小值 */ boolean b = employees.stream().allMatch((e) -> e.getStatus().equals(Employee.Status.BUSY)); System.out.println(b); boolean b1 = employees.stream().anyMatch((e) -> e.getStatus().equals(Employee.Status.BUSY)); System.out.println(b1); boolean b2 = employees.stream().noneMatch((e) -> e.getStatus().equals(Employee.Status.BUSY)); System.out.println(b2); Optional<Employee> first = employees.stream().findFirst(); System.out.println(first.get()); Optional<Employee> any = employees.parallelStream().findAny(); System.out.println(any.get()); long count = employees.stream().count(); System.out.println(count); Optional<Employee> max = employees.stream().max(Comparator.comparingDouble(Employee::getSalary)); System.out.println(max.get()); Optional<Double> min = employees.stream().map(Employee::getSalary).min(Double::compareTo); System.out.println(min.get()); }
2.归约
@Test public void test6(){ /** * 终止操作 * 2.归约 * T reduce(T identity, BinaryOperator<T> accumulator):将流中元素反复结合起来(类似累加),得到一个值;identity:初始值,BinaryOperator<T>:二元操作 */ List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Integer sum = list.stream().reduce(0, (x, y) -> x + y); System.out.println(sum); }
3.收集
@Test public void test7(){ /** * 终止操作 * 2.收集 * collect:将流转换为其他形式,接收一个Collector接口的实现,用于给Stream中元素做汇总的方法 * Collector:接口中方法的实现决定了如何对流执行收集操作 * Collectors:实现类提供了很多静态方法,可以方便创建收集器实例 * 1.mapping * 2.join() * 3.toList() * 4.toSet() * 5.toMap() * 6.toConcurrentMap() * 7.toCollection() * 8.counting() * 9.averagingDouble * 10.summarizingDouble * 11.maxBy() * 12.minBy() */ //映射 List<Person> list = new ArrayList<>(); list.add(new Person("Ram", 30)); list.add(new Person("Shyam", 20)); list.add(new Person("Shiv", 20)); list.add(new Person("Mahesh", 30)); String nameByAge = list.stream().collect(Collectors.mapping(Person::getName, Collectors.joining(",", "[", "]"))); System.out.println(nameByAge); nameByAge = list.stream().map(person -> person.getName()).collect(Collectors.joining(",", "[", "]")); System.out.println(nameByAge); Map<Integer, String> nameByAgeMap = list.stream().collect( Collectors.groupingBy(Person::getAge, Collectors.mapping(Person::getName, Collectors.joining(",", "[", "]")))); nameByAgeMap.forEach((k, v) -> System.out.println("Age:" + k + " Persons: " + v)); //连接符,前缀,后缀 List<String> list1 = Arrays.asList("A","B","C","D"); String result= list1.stream().collect(Collectors.joining("-","[","]")); System.out.println(result); List<String> collect = employees.stream() .map(Employee::getName) .collect(Collectors.toList()); collect.forEach(System.out::println); Set<String> collect1 = employees.stream() .map(Employee::getName) .collect(Collectors.toSet()); collect1.forEach(System.out::println); //Duplicate key:使用toMap()的重载方法,如果已经存在则不再修改,直接使用上一个数据 //(e1, e2) -> e1 这里使用的箭头函数,也就是说当出现了重复key的数据时,会回调这个方法,可以在这个方法里处理重复Key数据问题 Map<String, Double> collect8 = employees.stream().collect(Collectors.toMap(Employee::getName, Employee::getSalary, (e1, e2) -> e1)); for (Map.Entry<String, Double> stringDoubleEntry : collect8.entrySet()) { System.out.println(stringDoubleEntry.getKey() + ":" + stringDoubleEntry.getValue()); } ConcurrentMap<String, Double> collect9 = employees.stream().collect(Collectors.toConcurrentMap(Employee::getName, Employee::getSalary, (e1, e2) -> e1)); for (Map.Entry<String, Double> stringDoubleEntry : collect9.entrySet()) { System.out.println(stringDoubleEntry.getKey() + ":" + stringDoubleEntry.getValue()); } HashSet<String> collect2 = employees.stream() .map(Employee::getName) .collect(Collectors.toCollection(HashSet::new)); collect2.forEach(System.out::println); Long collect3 = employees.stream() .collect(Collectors.counting()); System.out.println(collect3); Double collect4 = employees.stream() .collect(Collectors.averagingDouble(Employee::getSalary)); System.out.println(collect4); //包含总数,总和,最小值,平均值,最大值 DoubleSummaryStatistics collect5 = employees.stream() .collect(Collectors.summarizingDouble(Employee::getSalary)); System.out.println(collect5); Optional<Employee> collect6 = employees.stream() .collect(Collectors.maxBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()))); System.out.println(collect6.get()); Optional<Employee> collect7 = employees.stream() .collect(Collectors.minBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()))); System.out.println(collect7.get()); }
4.分组
@Test public void test8(){ /** * 1.分组 * 2.多级分组 */ Map<Employee.Status, List<Employee>> collect = employees.stream().collect(Collectors.groupingBy(Employee::getStatus)); for (Map.Entry<Employee.Status, List<Employee>> statusListEntry : collect.entrySet()) { System.out.println(statusListEntry.getKey() + ":" + statusListEntry.getValue()); } Map<Employee.Status, Map<String, List<Employee>>> collect1 = employees.stream().collect(Collectors.groupingBy(Employee::getStatus, Collectors.groupingBy((e) -> { if (e.getAge() <= 35) { return "青年"; } else if (e.getAge() <= 50) { return "中年"; } else { return "老年"; } }))); for (Map.Entry<Employee.Status, Map<String, List<Employee>>> statusMapEntry : collect1.entrySet()) { System.out.println(statusMapEntry.getKey() + ":" + statusMapEntry.getValue().entrySet()); } }
5.分区
@Test public void test9(){ /** * 分区 true和false分区,满足条件的一区,不满足条件的一区 */ Map<Boolean, List<Employee>> collect = employees.stream().collect(Collectors.partitioningBy((e) -> e.getSalary() > 8000)); for (Map.Entry<Boolean, List<Employee>> booleanListEntry : collect.entrySet()) { System.out.println(booleanListEntry.getKey() + ":" + booleanListEntry.getValue()); } }
4.并行流与顺序流
- 并行流是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流
- Java8中将并行流进行了优化,可以更方便的对数据进行并行操作,Stream API可以声明性地通过parallel()与sequential()在并行流与顺序流之间进行切换
Fork/Join框架
- 注意:需要继承RecursiveTask
- 概念:在必要的情况下将一个大任务,进行拆分(fork)成若干个小任务,拆到不可再拆时,再将一个个的小任务运算的结果进行join汇总
- Fork/Join框架与传统线程池的区别
- 1.Fork/Join框架采用工作窃取模式(work-stealing)
- 即:当执行新的任务时它可以将其拆分,分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中
- 2.相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上,在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态;而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行,那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行,这种方式减少了线程的等待时间,提高了性能
package domains; import java.util.concurrent.RecursiveTask; public class ForkJoinCalculate extends RecursiveTask<Long> { private static final long serialVersionUID = -8301484471666236858L; private long start; //开始值 private long end; //结束值 private static final long THRESHOLD = 10000; //拆分终止值:拆分到什么时候才结束 public ForkJoinCalculate(long start, long end) { this.start = start; this.end = end; } @Override protected Long compute() { long length = end - start; //临界值:如果小于等于该值就不再拆分 if(length <= THRESHOLD){ long sum = 0; for (long i = start; i <= end; i++) { sum += i; } return sum; }else { //拆分 //一半一半的拆 long middle = (start + end) / 2; ForkJoinCalculate left = new ForkJoinCalculate(start, middle); left.fork(); //拆分子任务,同时压入线程队列 ForkJoinCalculate right = new ForkJoinCalculate(middle+1, end); right.fork(); //合并 return left.join() + right.join(); } } }
import domains.ForkJoinCalculate; import org.junit.Test; import java.time.Duration; import java.time.Instant; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinTask; import java.util.stream.LongStream; public class TestForkJoin { @Test public void test1(){ Instant start = Instant.now(); ForkJoinPool pool = new ForkJoinPool(); ForkJoinTask<Long> task = new ForkJoinCalculate(0,1000000000L); Long sum = pool.invoke(task); System.out.println("累加和为:" + sum); Instant end = Instant.now(); System.out.println("消耗时间为:" + Duration.between(start, end).toMillis()); /** * CPU利用率:100% * 累加和为:500000000500000000 * 消耗时间为:111 */ } @Test public void test2(){ Instant start = Instant.now(); long sum = 0L; for (int i = 0; i < 1000000000L; i++) { sum += i; } System.out.println("累加和为:" + sum); Instant end = Instant.now(); System.out.println("消耗时间为:" + Duration.between(start, end).toMillis()); /** * CPU利用率:20%左右 * 累加和为:499999999500000000 * 消耗时间为:364 */ } @Test public void test3(){ /** * java8并行流 */ Instant start = Instant.now(); long sum = LongStream.rangeClosed(0, 1000000000L) .parallel() //并行流 .reduce(0, Long::sum); System.out.println("累加和为:" + sum); Instant end = Instant.now(); System.out.println("消耗时间为:" + Duration.between(start, end).toMillis()); /** * CPU利用率:100%左右 * 累加和为:500000000500000000 * 消耗时间为:136 */ } }
5.Optional类
- java.util.Optional类是一个容器类,代表一个值存在或不存在
- 原来用null表示一个值不存在,现在Optional可以更好的表达这个概念,并且可以避免空指针异常
package domains; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class Godness { private String name; }
package domains; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class Man { private Godness godness; }
package domains; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Optional; @Data @AllArgsConstructor @NoArgsConstructor public class NewMan { //可能为空的属性用Optional包装 //需要初始值,不能为null private Optional<Godness> godness = Optional.empty(); }
import domains.Employee; import domains.Godness; import domains.Man; import domains.NewMan; import org.junit.Test; import java.util.Arrays; import java.util.List; import java.util.Optional; public class TestOptional { @Test public void test1(){ /** * Optional.of(T t):创建一个Optional实例,不能传递null会报空指针异常,可以快速定位 * Optional.empty():创建一个空的Optional实例 * Optional.ofNullable(T t):若t不为null,创建Optional实例,否则创建空实例 * isPresent():判断是否包含值 * orElse(T t):如果调用对象包含值,返回该值,否则返回t * orElseGet(Supplier s):如果调用对象包含值,返回该值,否则返回s获取的值;和orElse区别是可以通过供给型接口的实现类实现更多功能 * map(Function f):如果有值对其处理,并返回处理后的Optional,否则返回Optional.empty() * flatMap(Function mapper):与map类似,要求返回值必须是Optional * filter:接收一个 Predicate 来对 Optional 中包含的值进行过滤,如果包含的值满足条件,那么还是返回这个 Optional;否则返回 Optional.empty */ Optional<Employee> optional = Optional.of(new Employee()); Employee employee = optional.get(); System.out.println(employee); // Optional<Object> empty = Optional.empty(); // System.out.println(empty.get()); // Optional<Object> o = Optional.ofNullable(null); // System.out.println(o.get()); Optional<Object> o = Optional.ofNullable(null); if(o.isPresent()){ System.out.println(o.get()); } Optional<Employee> objectOptional = Optional.ofNullable(null); System.out.println(objectOptional.orElse(new Employee(25))); System.out.println(objectOptional.orElseGet(() -> new Employee("李四",3,35.5, Employee.Status.BUSY))); Optional<Employee> employee1 = Optional.ofNullable(new Employee("李四",3,35.5, Employee.Status.BUSY)); Optional<String> optional1 = employee1.map(Employee::getName); System.out.println(optional1.get()); Optional<String> s = employee1.flatMap((e) -> Optional.of(e.getName())); System.out.println(s.get()); List<Employee> employees = Arrays.asList( new Employee("张三",8,100, Employee.Status.BUSY), new Employee("李四",12,500, Employee.Status.BUSY), new Employee("王五",18,3000, Employee.Status.FREE), new Employee("赵六",24,8000, Employee.Status.FREE), new Employee("赵九",24,6000, Employee.Status.FREE), new Employee("田七",35,15000, Employee.Status.VOCATION), new Employee("吴八",40,25000, Employee.Status.FREE) ); Optional<Employee> optional2 = Optional.ofNullable(employees.get(5)).filter(e -> e.getAge() > 24); System.out.println(optional2.get()); } @Test public void test2(){ Man man = new Man(); // String godnessName = getGodnessName(man); String godnessName = getGodnessName2(Optional.ofNullable(null)); System.out.println(godnessName); } public String getGodnessName(Man man){ //空指针异常 return man.getGodness().getName(); //传统if嵌套过深 if(man != null){ Godness godness = man.getGodness(); if(godness != null){ return godness.getName(); } } return "测试"; } public String getGodnessName2(Optional<NewMan> man){ //Optional return man.orElse(new NewMan()) .getGodness() .orElse(new Godness("测试")) .getName(); } }
6.接口中的默认方法与静态方法
- Java8中允许接口中包含具有具体实现的方法,该方法称为默认方法,默认方法使用default关键字修饰
接口默认方法的类优先原则
- 若一个接口定义了一个默认方法,而另外一个父类或接口又定义了一个同名的方法时
- 1.选择父类中的方法。如果一个父类提供了具体的实现,那么接口中具有相同名称和参数的默认方法会被忽略
- 2.接口冲突。如果一个父接口提供一个默认方法,而另一个父接口也提供了一个具有相同名称和参数列表的方法(不管方法是否是默认方法),那么实现类必须选择一个接口或者覆盖该方法来解决冲突
package dao; public interface MyFun1 { default String getName(){ return "myFun1"; } static void show(){ System.out.println("接口中的静态方法"); } }
package dao; public interface MyFun2 { default String getName(){ return "myFun2"; } }
package domains; public class MyClassFun { public String getName(){ return "MyClassFun"; } }
package domains; import dao.MyFun1; import dao.MyFun2; //public class SubClass extends MyClassFun implements MyFun1 { //} public class SubClass implements MyFun1, > MyFun2 { @Override public String getName() { return MyFun1.super.getName(); } }
import domains.SubClass; import org.junit.Test; public class TestDefault { @Test public void test1(){ SubClass subClass = new SubClass(); String name = subClass.getName(); System.out.println(name); MyFun1.show() } }
7.传统时间格式化的线程安全问题
- java.util.Date() JDK1.0版本
- java.util.Calender() JDK1.1版本
- java.util.TimeZone()
- java.text.SimpleDateFormat
- 声明不规范,不安全
8.新时间与日期API
- java.time包下
- java.time.
- java.time.format
- java.time.temporal
- java.time.zone
- 不可变,会产生新的实例
- 线程安全
9.重复注解与类型注解
常见面试题
1.JDK和JRE的区别
- 1.JDK(Java SE开发工具集):包括
JRE
和命令行开发工具(编译,运行,调试Java
程序所需要的基本工具)- 2.JRE(运行环境):提供
Java
虚拟机和运行Java
应用程序所必需的类库- 3.JRE与JDK的区别
- 1.如果只需要在某种操作系统下运行
Java
应用程序,则安装支持该操作系统的JRE
即可- 2.如果不仅要运行
Java
应用程序,还要开发Java
应用程序,就需要安装支持该操作系统的JDK
2.a+=b 和 a=a+b 的区别
- 1.使用
+=,-=,*=,/=,%=
运算符进行赋值时,强制类型转换会自动完成,程序不需要做任何显式地声明- 2.当
a
和b
结果类型不一致时,a+=b
会自动完成类型强制转换,而a=a+b
会报错- 3.当
a
和b
结果类型一致时,两者没区别
3.==和equals的区别
- 1.
==
- 1.基本数据类型的变量存放在虚拟机栈的局部变量表中的是值,
==
在比较基本数据类型时,比较的是变量值是否相同- 2.引用数据类型的变量存放在虚拟机栈的局部变量表中的是对象引用/堆空间的内存地址,比较的是堆地址是否相同
- 2.
equals
- 1.
equals
用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object
类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object
类中的方法,而Object
中的equals
方法返回的是==
的判断
- 2.
Java
中所有的类都是继承与Object
基类,Object
类中定义了一个equals
方法,这个方法的初始行为是比较对象的内存地址
,但在一些类库中已经重写了这个方法,所以不再是比较类在堆中的地址,(一般都是用来比较对象的成员变量值是否相同String
,Integer
,Date
等类)
4.equals和hashcode关系
- 1.
equals
方法和hashCode
方法是Object
类中的两个基础方法,它们共同协作来判断两个对象是否相等- 2.如果两个对象相等,即
equals()
结果为true
,那么二者的hashCode()
也必须相同;但如果两个对象的hashCode
相同,则二者不一定相同- 3.并不要求根据
equals()
不相等的两个对象,调用二者各自的hashCode()
方法必须产生不同的结果,但是对于不同的对象产生不同的hashcode
,可能会提高hashtable
的性能- 4.当对比两个对象是否相等时,可以先使用
hashCode
进行比较,如果比较的结果是true
,可以使用equals
再次确认两个对象是否相等,如果比较的结果是true
,那么这两个对象就是相等的,否则其他情况就认为两个对象不相等;这样大大的提升了对象比较的效率,这也是为什么Java
设计使用hashCode
和equals
协同的方式,来确认两个对象是否相等的原因- 5.不能直接使用
hashCode
就确定两个对象是否相等,因为不同对象的hashCode
可能相同,但hashCode
不同的对象一定不相等,所以使用hashCode
可以起到快速初次判断对象是否相等的作用
5.重写equals时为什么一定要重写hashCode
- 1.重写
equals
方法
- 1.
Object
类中的equals
方法用于检测一个对象是否等于另外一个对象- 2.
Object
类中,这个方法将判断两个对象是否具有相同的引用,如果两个对象具有相同的引用,则一定是相等的
- 3.通过上述源码和
equals
的定义可以看出,大多数情况下equals
的判断是没有什么意义的- 4.例:使用
Object
中的equals
比较两个自定义的对象是否相等完全没有意义(因为无论对象是否相等,结果都是false
),因此通常要判断两个对象是否相等,一定要重写equals
方法,这就是要重写equals
方法的原因
- 2.重写
hashCode
方法
- 1.
hashCode
是一个散列码,它是由对象推导出的一个整型值,并且这个值为任意整数,包括正数或负数- 2.散列码是没有规律的,如果
x
和y
是两个不同的对象,x.hashCode()
与y.hashCode()
基本上不会相同;但如果a
和b
相等,则a.hashCode()
一定等于b.hashCode()
- 3.相同的值
hashCode
一定相同
- 4.不同的值
hashCode
也有可能相同
- 3.一起重写
- 1.
Set
集合是用来保存不同对象的,如果Set 集合中存储的是只重写了equals
方法的自定义对象时会出现异常
- 2.如上图所示即使两个对象是相等的,
Set
集合却没有将二者进行去重与合并,这就是重写了equals
方法,但没有重写hashCode
方法的问题所在- 4.问题原因
- 1.如果只重写了
equals
方法,那么默认情况下,Set
进行去重操作时,会先判断两个对象的hashCode
是否相同,此时因为没有重写hashCode
方法,所以会直接执行Object
中的hashCode
方法,而Object
中的hashCode
方法对比的是两个不同引用地址的对象,所以结果是false
,那么equals
方法就不会执行,直接返回的结果就是false
:两个对象不是相等的,于是就在Set
集合中插入了两个相同的对象。- 2.但如果在重写
equals
方法时,也重写了hashCode
方法,那么执行判断时会先执行重写的hashCode
方法,此时对比的是两个对象的所有属性的hashCode
是否相同,于是调用hashCode
返回的结果就是true
,再去调用equals
方法,发现两个对象确实是相等的,于是就返回true
,因此Set
集合不会存储两个一样的数据
- 5.具体细节可参考
集合及其使用方式
文章中的HashSet
6.String的底层实现
- 1.
String
类的一个最大特性是不可修改性,其不可修改的原因是在String
内部定义了一个常量char数组
,因此每次对字符串的操作实际上都会另外分配一个新的常量数组空间- 2.
Java
打印String
对象的地址:当使用System.out.println()
方法打印String
类型对象时,会输出String
对象代表的字符串,并不会输出对象的地址- 3.一般被打印对象的形式为:
全限定类名+@+十六进制数组成
- 4.性能优化:对于直接相加字符串
JVM
在编译期便确定其值,如I+love+java
的字符串相加,在编译期间便被优化成了Ilovejava
;对于间接相加(即包含字符串引用或方法调用) 效率要比直接相加低,因为在编译期不会对引用变量或方法进行优化
- 5.
String str = new String("abc")
创建了多少个对象
![]()
- 1.从字节码看
new
只调用了一次,也就是说只创建了一个对象- 2.这段代码在
运行期间
确实只创建了一个对象,即在堆
上创建了abc
对象- 3.为什么一般说创建了
2
个对象:该段代码执行过程
和类的加载过程
是有区别的,在类加载的过程中,确实在运行时常量池
中创建了一个abc
对象,而在代码执行过程
中确实只创建了一个String
对象,因此这个问题合理的解释是2个
7.String,StringBuilder,StringBuffer的区别
- 1.
String
是常量字符数组,而StringBuilder
和StringBuffer
是变量字符数组- 2.
String
没有默认初始化容量,而StringBuilder
和StringBuffer
默认初始化容量为16
- 3.
StringBuilder
线程不安全,StringBuffer
线程安全,因为StringBuffer
的大部分方法都使用synchronized
关键字加锁
8.final、finalize和finally的区别
- 1.
final
- 1.
final
是一个修饰符,其可以修饰类
,方法
和属性
- 2.
final
修饰的类
,不能作为父类被子类继承,因此一个类不能既被abstract
声明,又被final
声明- 3.
final
修饰的变量
,称为常量,不能被修改,必须在声明时赋值- 4.
final
修饰的方法
,不能被重写- 2.
finally
- 1.
finally
是异常处理时提供来执行最终结果的finally
代码块- 2.不管有没有异常被抛出、捕获,
finally
代码块都会被执行- 3.
finalize
- 1.
finalize
是方法名,属于Object
类- 2.
finalize()
方法用于垃圾回收,一般情况下不需要实现- 3.当对象被回收的时候需要释放一些资源;如
socket
链接,在对象初始化时创建,整个生命周期内有效,那么需要实现finalize
方法关闭这个链接- 4.但是当调用
finalize
方法后,并不意味着GC
会立即回收该对象,所以有可能真正调用的时候,对象又不需要回收了,然后到了真正要回收的时候,因为之前调用过一次,这次又不会调用了,产生问题,所以不推荐使用finalize
方法
9.volatile
- 1.并发编程中通常会遇到以下三个问题
- 1.原子性问题
- 1.
原子性
:指一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断
,要么就都不执行
- 2.可见性问题
- 1.
可见性
:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
- 3.有序性问题
- 1.
有序性
:指程序执行的顺序按照代码的先后顺序执行- 2.
指令重排序
:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的- 3.
指令重排序
不会影响单个线程的执行,但是会影响到线程并发执行的正确性- 2.要想
并发程序正确地执行
,必须要保证原子性、可见性以及有序性;只要有一个没有被保证,就有可能会导致程序运行不正确- 3.
Java
解决以上三个问题
- 1.
原子性
- 1.
Java
内存模型只保证了基本读取和赋值
是原子性
操作,如果要实现更大范围操作的原子性,可以通过synchronized
和Lock
来实现;- 2.因为
synchronized
和Lock
能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性- 2.
可见性
- 1.
Java
提供了volatile
关键字来保证可见性,当一个共享变量被volatile
修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值- 2.而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性
- 3.通过
synchronized
和Lock
也能够保证可见性,synchronized
和Lock
能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性- 3.
有序性
- 1.
Java
内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性- 2.Java里面,可以通过
volatile
关键字来保证一定的有序性- 3.也可以通过
synchronized
和Lock
来保证有序性,因为synchronized
和Lock
保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性- 4.
volatile
关键字的两层语义
- 1.一旦一个共享变量(类的成员变量、类的静态成员变量)被
volatile
修饰之后,那么就具备了两层语义:
- 1.保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,新值对其他线程来说是立即可见的
- 2.禁止进行指令重排序,但是只能对当前修饰的操作保证,其他操作无法保证,所以volatile能在一定程度上保证有序性
- 5.
volatile
关键字无法保证对变量的任何操作都是原子性
- 6.具体可参考:
https://www.cnblogs.com/ustc-anmin/p/11434769.html
10.BigDecimal
11.new关键字的作用
- 1、为对象分配内存空间
- 2、引起对象构造方法的调用
- 3、为对象返回一个引用
值传递:在方法调用时,传递的是实际参数的值的副本。当参数变量被赋予新的值时,只会修改副本的值,不会影响原始值。Java 中的基本数据类型都采用值传递方式传递参数变量的值。
引用传递:在方法调用时,传递的是实际参数的引用(即内存地址)。当参数变量被赋予新的值时,会修改原始值的内容。Java 中的对象类型采用引用传递方式传递参数变量的值。