专门针对Java核心基础的面试题,因为搜到的题目都非常零散,所以打算自己整理以下,尽量做得全面有条理。
如有错误请评论区指出,感谢。
如果各位有发现新的关于JavaSE的题目,请在评论区留下题目,我会完善我的文档,非常感谢!
最新更新时间:2024-11-14
Java的特点
Java语言是面向对象的。
Java语言是健壮的。Java的强类型机制、异常处理、垃圾的自动收集等是Java程序健壮性的重要保证。
Java语言是跨平台性的。(跨平台性:对于不同的平台,有不同的虚拟机 JVM。Java虚拟机机制屏蔽了底层运行平台的差别,实现了“一次编译,到处运行”。)
Java是一种混合型语言,它既包含解释型语言的特性,也包含编译型语言的特性。
Java的执行过程体现了其混合型语言的特性。首先,Java源代码经过javac 编译后,会生成class文件,这些文件在形式上非常接近机器语言,但并非直接的机器语言。在Java程序运行时,class文件被加载到JVM中,JVM逐条解释 class中包含的指令来执行。这一过程使得Java在某种程度上表现为解释型语言。然而,Java引入了**JIT(Just In Time)**技术,即在运行时,JIT将解释器翻译后的机器码缓存起来,以备下次使用。这种情况下,经过JIT缓存后的代码可以达到编译型语言的效率,从而不再需要经过JVM的逐行解释,实现了编译型语言的效果。因此,Java并非严格意义上的解释型或编译型语言,而是二者共存,相互促进的语言。
此外,Java的这种混合特性也体现在其跨平台性上。由于Java首先由编译器编译成.class(字节码)文件,然后通过JVM(相当于解释器)从.class文件中读一行解释执行一行,这种机制使得Java程序可以在不同操作系统上运行,只要该操作系统上有相应的JVM。这种跨平台性是Java混合型语言特性的一个重要体现。
编译型语言和解释型语言(了解)
编译型语言:是一种使用编译器一次性地将源代码编译成二进制机器码,然后再执行的高级语言。
编译器(compiler):是一种把高级语言一次性翻译成机器语言的一种软件(也可称之为一个程序)。
解释型语言:是一种使用解释器一行一行地将源代码翻译成二进制机器码,进行解释执行的语言。
解释器(Interpreter):是一种把高级语言一行一行地翻译成机器语言的一种软件。
综上所述,Java既不是纯粹的解释型语言,也不是纯粹的编译型语言,而是结合了两种语言的优点,通过JIT技术实现了高效执行的同时保持了跨平台的能力。
为什么编译型语言不能跨平台?
原因一:可执行程序不能跨平台
不同操作系统对可执行文件的内部结构有着截然不同的要求,彼此之间不能兼容。
另外,相同操作系统的不同版本之间也不一定兼容,比如不能将 x64 程序(Windows 64 位程序)拿到 x86 平台(Windows 32 位平台)下运行。但是反之一般可行,因为 64 位 Windows 对 32 位程序作了很好的兼容性处理。
原因二:源代码不能跨平台
不同平台支持的函数、类型、变量等都可能不同,基于某个平台编写的源代码一般不能拿到另一个平台下编译。我们以C语言为例来说明。
【实例1】在C语言中要想让程序暂停可以使用“睡眠”函数,在 Windows 平台下该函数是 Sleep(),在 Linux 平台下该函数是 sleep(),首字母大小写不同。其次,Sleep() 的参数是毫秒,sleep() 的参数是秒,单位也不一样。
以上两个原因导致使用暂停功能的C语言程序不能跨平台,除非在代码层面做出兼容性处理,非常麻烦。
【实例2】虽然不同平台的C语言都支持 long 类型,但是不同平台的 long 的长度却不同,如Windows 64 位平台下的 long 占用 4 个字节,Linux 64 位平台下的 long 占用 8 个字节。
我们在 Linux 64 位平台下编写代码时,将 0x2f1e4ad23 赋值给 long 类型的变量是完全没有问题的,但是这样的赋值在 Windows 平台下就会导致数值溢出,让程序产生错误的运行结果。让人苦恼的,这样的错误一般不容易察觉,因为编译器不会报错,我们也记不住不同类型的取值范围。
为什么编译型语言不能跨平台?
原因一:可执行程序不能跨平台
不同操作系统对可执行文件的内部结构有着截然不同的要求,彼此之间不能兼容。
另外,相同操作系统的不同版本之间也不一定兼容,比如不能将 x64 程序(Windows 64 位程序)拿到 x86 平台(Windows 32 位平台)下运行。但是反之一般可行,因为 64 位 Windows 对 32 位程序作了很好的兼容性处理。
原因二:源代码不能跨平台
不同平台支持的函数、类型、变量等都可能不同,基于某个平台编写的源代码一般不能拿到另一个平台下编译。我们以C语言为例来说明。
【实例1】在C语言中要想让程序暂停可以使用“睡眠”函数,在 Windows 平台下该函数是 Sleep(),在 Linux 平台下该函数是 sleep(),首字母大小写不同。其次,Sleep() 的参数是毫秒,sleep() 的参数是秒,单位也不一样。
以上两个原因导致使用暂停功能的C语言程序不能跨平台,除非在代码层面做出兼容性处理,非常麻烦。
【实例2】虽然不同平台的C语言都支持 long 类型,但是不同平台的 long 的长度却不同,如Windows 64 位平台下的 long 占用 4 个字节,Linux 64 位平台下的 long 占用 8 个字节。
我们在 Linux 64 位平台下编写代码时,将 0x2f1e4ad23 赋值给 long 类型的变量是完全没有问题的,但是这样的赋值在 Windows 平台下就会导致数值溢出,让程序产生错误的运行结果。让人苦恼的,这样的错误一般不容易察觉,因为编译器不会报错,我们也记不住不同类型的取值范围。
关于面向对象
什么是面向对象?
(答题思路:要对比面向过程阐述面向对象更全面,这是两种不同的处理问题的角度。)
面向过程比较的直接高效,思考这个问题本身,你需要做什么,就按照顺序写代码。
面向对象需要对问题拆解成一个个模块,首先要分析这个需求中有哪些对象,然后每一个对象各自的任务,会有一个分析的过程。
所以面向对象它更易于复用、扩展、维护,但在性能这方面,面向过程更加有优势。
比如:洗衣机洗衣服
面向过程会将任务拆解成一系列的步骤(函数),1、打开洗衣机–>2、放衣服–>3、放洗衣粉–>4、清洗–>5、烘干
面向对象会拆出人和洗衣机两个对象
人:打开洗衣机,放衣服,放洗衣粉
洗衣机:清洗,烘干
面向对象三大特性–封装、继承、多态
(答题思路:回答出三个基本点,每个基本点再阐述一些应用或细节。)
封装:
封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项,内部细节对外部调用透明,外部调用无需修改或者关心内部实现。
好处:
1、隐藏细节 对于调用者不需要关心细节,他们只需要接口信息就好
2、可以对数据进行验证,保证健壮性,安全合理
关于封装的应用:
1、javabean的属性私有,提供get/set对外访问,因为属性的赋值或者获取逻辑只能由javabean本身决定。而不能由外部胡乱修改。
private string name;
public void setName(string name){
this.name = "tuling_"+name;
}
该name有自己的命名规则,明显不能由外部直接赋值。
2、orm框架
操作数据库,我们不需要关心链接是如何建立的、sql是如何执行的,只需要引入mybatis,调用方法即可。
继承:
继承基类的方法,并做出自己的改变或扩展,子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的。
好处:
提高代码复用性
代码的扩展性和维护性提高了
多态:(关于对象的多态如果时间允许可以展开讲讲,否则就精简回答)
多态是方法或对象具有多种形态。允许不同类型的对象对同一方法进行不同的实现。具体来说,多态性指的是通过父类的引用变量来引用子类的对象,从而实现对不同对象的统一操作。
多态的前提是两个对象(类)存在继承关系,多态是建立在封装和继承基础之上的。
继承时的访问成员的特性
子类中调用属性:
(1)首先看子类是否有该属性
(2)如果子类有这个属性,并且可以访问,则返回;如果子类没有这个属性,就看父类有没有这个属性;
(3)如果父类有该属性,并且可以访问,就返回信息;如果父类没有就按照(3)的规则,继续找上级父类,直到Object。
提示:如果查找方法的过程中,找到了,但是不能访问,则报错,cannot access。如果查找方法的过程中,没有找到,则提示属性不存在
子类中调用方法同理。
多态、动态绑定
多态
(结合上面的概念描述回答)
多态实现必须满足以下条件:
1、继承关系
存在继承关系的类之间才能够使用多态性。多态性通常通过一个父类用变量引用子类对象来实现。
2、方法重写
子类必须重写(Override)父类的方法。通过在子类中重新定义和实现父类的方法,可以根据子类的特点行为改变这个方法的行为。
3、父类引用指向子类对象
使用父类的引用变量来引用子类对象。这样可以实现对不同类型的对象的统一操作,而具体调用哪个子类的方法会在运行时多态决定。
具体体现:
1、方法的多态:重写和重载。
2、对象的多态:前提是两个对象(类)存在继承关系。父类的引用变量来引用子类的对象,从而实现对不同对象的统一操作。
一个对象的编译类型与运行类型可以不一致;
编译类型在定义对象时,就确定了,不能改变,而运行类型是可以变化的;
编译类型看定义对象时 = 号的左边,运行类型看 = 号的右边。
// 也是向上转型的格式
父类类型 变量名 = new子类对象;
变量名.方法名();
缺点:无法调用子类特有的功能。但可以通过向下转型调用。
多态的转型:
向上转型:
本质:父类的引用指向子类的对象。
好处:可以以统一的方式处理不同类型的对象,实现代码的灵活性和可扩展性。
规则和特点:
编译类型看左边,运行类型看右边;
可以调用父类的所有成员(需遵守访问权限);
不能调用子类的特有成员;
运行效果看子类的具体实现(动态绑定)。
向下转型:
本质:一个已经向上转型的子类对象,将父类引用转为子类引用。
子类类型 引用名 = (子类类型) 父类引用;
//用强制类型转换的格式,将父类引用类型转为子类引用类型
在某些情况下,当一个对象被向上转型后,它的具体类型信息会丢失,只保留了父类类型的信息。如果我们需要访问子类中特有的成员或调用子类重写的方法,就需要使用向下转型。
需要注意的是,向下转型是有风险的,因为转换的对象必须是实际上是子类对象才能成功,否则会在运行时抛出 ClassCastException 异常。
特点:
只能强制转换父类的引用,不能强制转换父类的对象;
要求父类的引用必须指向的是当前目标类型的对象;
当向下转型后,可以调用子类类型中所有的成员。
多态性的优点:
灵活性和可扩展性:多态性使得代码具有更高的灵活性和可扩展性。通过使用父类类型的引用变量,可以以统一的方式处理不同类型的对象,无需针对每个具体的子类编写特定的代码。
代码复用:多态性可以促进代码的复用。可以将通用的操作定义在父类中,然后由子类继承并重写这些操作。这样一来,多个子类可以共享相同的代码逻辑,减少了重复编写代码的工作量。
可替换性:多态性允许将一个对象替换为其子类的对象,而不会影响程序的其他部分。这种可替换性使得系统更加灵活和可维护,可以方便地添加新的子类或修改现有的子类,而无需修改使用父类的代码。
代码扩展性:通过引入新的子类,可以扩展现有的代码功能,而无需修改现有的代码。这种可扩展性使得系统在需求变化时更加容易适应和扩展。
多态性的缺点:
运行时性能损失:多态性需要在运行时进行方法的动态绑定,这会带来一定的性能损失。相比于直接调用具体的子类方法,多态性需要在运行时确定要调用的方法,导致额外的开销。
代码可读性下降:多态性使得代码的行为变得更加动态和不确定。在某些情况下,可能需要跟踪代码中使用的对象类型和具体的方法实现,这可能降低代码的可读性和理解性。
限制访问子类特有成员:通过父类类型的引用变量,只能访问父类及其继承的成员,无法直接访问子类特有的成员。如果需要访问子类特有的成员,就需要进行向下转型操作,这增加了代码的复杂性和维护的难度。
虽然多态性具有一些缺点,但在大多数情况下,其优点远远超过缺点,使得代码更具灵活性、可扩展性和可维护性。因此,多态性在Java编程中被广泛应用。
多态的应用:
1、多态数组:数组的定义为父类类型,里面保存的实际元素类型为子类类型。
2、多态参数:方法定义的形参类型为父类类型,实参类型允许为子类类型
动态绑定
当调用对象方法的时候,该方法会和该对象的运行类型绑定。
当调用对象属性时,没有动态绑定机制,即哪里声明,哪里使用。
动态绑定引发的问题:
避免在构造方法中调用重写的方法,如果这个方法被子类重写,就会触发动态绑定,但是此时子类对象还没构造完成,可能会出现一些隐藏的但是又极难发现的问题。所以在构造函数内,尽量避免使用实例方法,除了 final 和 private 方法,因为它们无法被子类重写。
泛型
问:项目中对泛型的使用
1、JavaWeb-封装公共响应值给前端
在实际开发中,后端给前端的响应值必须要有一层封装,封装里边有状态码、错误信息、接口数据等。前端会先判断状态码,如果是成功,则获取数据;如果是失败,直接奖错误信息提示给用户。
有了这个公共的响应值,前后端交互会变得统一,这样会很便捷高效,会减少很多问题。
如下面代码,因为返回数据可能有多种形式,比如单个对象、数组、集合等,因此采用泛型。
package com.knife.common.entity;
import com.knife.common.constant.ResultCode;
import lombok.Data;
@Data
public class ResultWrapper<T> {//泛型
private Boolean success = true;
private Integer code;
private T data;
private String message;
private ResultWrapper() {
}
public static <T> ResultWrapper<T> success() {
return success(null);
}
public static <T> ResultWrapper<T> success(T data) {
return assemble(ResultCode.SUCCESS.getCode(), true, data);
}
public static <T> ResultWrapper<T> error() {
return error(null);
}
public static <T> ResultWrapper<T> error(T data) {
return assemble(ResultCode.SYSTEM_FAILURE.getCode(), false, data);
}
public ResultWrapper<T> data(T data) {
this.setData(data);
return this;
}
public ResultWrapper<T> message(String message) {
this.setMessage(message);
return this;
}
public ResultWrapper<T> code(int code) {
this.setCode(code);
return this;
}
public static <T> ResultWrapper<T> assemble(int code, boolean success, T data) {
ResultWrapper<T> resultWrapper = new ResultWrapper<>();
resultWrapper.setCode(code);
resultWrapper.setSuccess(success);
resultWrapper.setData(data);
return resultWrapper;
}
}
//使用:
return ResultWrapper.success(data);
return ResultWrapper.success(data).message("访问成功");
return ResultWrapper.error().message("访问失败");
//结果状态码
import lombok.Getter;
@Getter
public enum ResultCode {
SUCCESS(1000, "访问成功"),
SYSTEM_FAILURE(1001, "系统异常"),
;
private final int code;
private final String description;
ResultCode(int code, String description) {
this.code = code;
this.description = description;
}
}
2、SpringBoot 复制对象的工具类
项目中经常遇到将List转化为其他类型的List的情况,比如:将List<User>转化为List<UserDTO>。
优点:一行代码即可转换。底层使用Spring的BeanUtils,很稳定。
(关于BeanUtils可看这篇文章)
import org.springframework.beans.BeanUtils;
public class BeanUtil {
public static void copyProperties(Object source, Object target) {
BeanUtils.copyProperties(source, target, getNullPropertyNames(source)); //JavaBean,DTO,要忽略的一些属性名
}
//获取值为空的所有属性名
private static String[] getNullPropertyNames(Object source) {
final BeanWrapper src = new BeanWrapperImpl(source);
PropertyDescriptor[] pds = src.getPropertyDescriptors();
Set<String> emptyNames = new HashSet<>();
for(PropertyDescriptor pd : pds) {
Object srcValue = src.getPropertyValue(pd.getName());
if (srcValue == null) emptyNames.add(pd.getName());
}
String[] result = new String[emptyNames.size()];
return emptyNames.toArray(result);
}
}
3、CountDownLatch的用法
看这篇文章
jdk jre jvm 及三者区别和联系
(分别简单描述三者之后)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8tlN16KH-1721242362128)(https://i-blog.csdnimg.cn/direct/20fe4203704c48b883c7e9474206ded8.png)]
JDK:java development kit 即Java开发工具,它是一个给开发人员使用的开发工具。
JRE:java runtime environment 即Java运行时环境,提供给要运行java程序的用户使用。(如果用户只需要运行java程序,则安装jre即可,而不必像开发人员一样必须安装jdk)
JVM:java virtual machine,即Java虚拟机,将class文件解释成这个机器码,使得操作系统能够执行。具有指令集并使用不同的存储区域,负责执行指令、管理数据、内存、寄存器,包含在JDK中。
三者的关系:
JDK相当于是由JRE加上java工具(如 javac、java、jconsole等等)组成的,即包含JRE文件夹;JRE有两个核心文件夹bin(JVM)和lib(java核心类库)。(即JDK包含JRE,JRE又包含JVM)
(.java文件)
👇通过java工具中的javac编译成–
(.class文件)
👇放到JVM中(不同OS有相应JVM,实现一处编译到处运行,即跨平台),JVM会根据lib目录中的类库解释该class文件,翻译成–
(机器码)
映射到当前的操作系统进行系统调用,最终让我们这个程序能够正常的跑起来。
重写、重载
重写(Override):子类重新定义和实现了从父类继承而来的方法,以改变方法的行为。子类通过重写方法可以提供自己特定的实现,使得父类方法的行为在子类对象的身上有不同的表现。
重载(Overload):在同一个类中,根据方法的参数列表的不同,定义多个具有相同名称但参数类型或个数不同的方法。
重写的规则:
1、方法名称、参数列表和返回类型必须与父类中被重写的方法相同。
2、重写的方法的访问修饰符的权限不能低于父类中被重写方法的访问修饰符权限。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected。
3、重写的方法不能抛出比父类中被重写的方法更多或更宽泛的异常。子类中重写的方法可以抛出相同的异常或更具体的异常,或者不抛出异常。
例如,如果父类的方法声明抛出 IOException,则子类中重写的方法可以抛出 IOException 或 FileNotFoundException,或者不抛出异常,但不能抛出比 IOException 更通用的异常,如 Exception。
4、重写的方法必须具有相同的方法体,或者可以进行方法体的扩展。
子类中重写的方法可以调用父类中被重写的方法,使用 super 关键字。
重载的规则:
1、方法名称相同:重载的方法必须具有相同的名称。
2、参数列表不同:重载的方法的参数列表必须不同。参数列表可以通过参数的类型、个数或顺序的不同来区分重载方法。
3、返回类型可相同也可不同:返回类型不是重载方法的区分标准。
4、方法的访问修饰符可相同也可不同。
5、方法的异常可相同也可不同。
重写和重载的区别:
定义位置:重载方法定义在同一个类中,而重写方法定义在父类和子类之间。
方法签名:重载方法具有相同的名称,但方法签名(参数类型和个数)不同。重写方法具有相同的名称和方法签名。
继承关系:重载方法不涉及继承关系,可以在同一个类中定义。重写方法是在子类中对父类方法的重新定义和实现。
目的:重载方法用于在同一个类中实现相似功能但具有不同参数的方法。重写方法用于子类重新定义父类方法以适应子类的特定需求。
确定调用:重载方法通过静态绑定在编译时确定调用,重写方法通过动态绑定在运行时确定调用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dLj8F4wo-1721242362138)(https://i-blog.csdnimg.cn/direct/3d7d94bf5dce45e6911040167f4d13cb.png)]
instanceof 比较操作符
用于判断对象的【运行类型】是否为XX类型或XX类型的子类型。
sleep()、wait()、join()、yield()的区别
(对象)锁(定)池
所有需要竞争某个资源(比如一个对象)同步锁的线程都会放在当前资源的锁池当中,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待cpu资源分配。
等待(锁定)池
当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁的(不在锁池中)。只有调用了notify()或notifyAlI()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中。
sleep()和wait()
这两个方法在并发编程中使用比较多,都是让线程暂停执行。
1.所属类不同
sleep是Thread线程类的静态方法,作用于当前线程,而wait是Object顶级类的普通方法,作用于对象本身。
2.应用场景不同
sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的协作和通信。
3.同步
sleep可以在任何地方使用,而wait只能在synchronized同步方法或者同步块中使用。
4.持有锁的状态不同
sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,当指定的时间到了又会自动恢复运行状态,线程不会释放对象锁。
调用wait()方法的时候,线程会释放对象锁,进入等待此对象的等待池,只有针对此对象调用notify()/notifyAll()方法后本线程才进入锁池准备。
5.相同点
它们都可以被interrupted方法中断。如果在睡眠sleep期间其他线程调用了这个线程的interrupt方法,那么这个线程也会抛出interruptexception异常返回,这点和wait是一样的。
6.唤醒条件
sleep不需要唤醒,等待超时或者调用interrupt方法就好了,但wait需要,或者也interrupt。
yield():让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。
join ():执行别的线程执行后本线程进入阻塞状态,例如在线程B中调用线程A的join (),那线程B会进入到阻塞队列,直到线程A结束或中断线程。
Thread.sleep(0)的意义
Thread.sleep()方法是java线程调度的一部分,让当前运行的线程暂停执行并进入到阻塞状态,让出CPU的执行权。这个方法的底层是调用操作系统的sleep或者nanosleep系统调用,操作系统会把这个线程挂起让出CPU的执行权给到其他线程或者进程,同时操作系统会设置一个定时器,当定时器时间一到,操作系统会再次唤醒这个线程。
Thread.sleep(0)虽然没有传递睡眠时长,但实际上还是会触发线程调度的切换,当前线程会从运行状态变为就绪状态,然后操作系统的调度器根据优先级来选择一个线程执行,如果有优先级更高的线程正在等待CPU的时间片,那么这个线程就会得到执行,如果没有,那么可能就会立即再次选择刚刚进入就绪状态的这个线程执行,具体的调度策略取决于操作系统层面的调度算法。
接口、抽象类
共同点
都不能被实例化。
都可以包含抽象方法,这些抽象方法用于定义接口,但不提供实现。
派生类必须实现未实现的抽象方法。
接口和抽象类都可以被用作其他类的基类。
接口与抽象类的区别
项 | 抽象类 | 接口 |
---|---|---|
继承与实现 | 子类使用extends关键字来继承抽象类。 只能继承1个抽象类。 | 子类使用关键字implements来实现接口。 可以实现多个接口。 |
构造方法 | 可以有构造方法。 | 不能有构造方法。 |
普通方法 | 允许有普通方法。 | 所有方法都必须是抽象的。 (JDK8后允许使用default、static定义非抽象方法) |
成员变量 | 允许有成员变量。 | 只允许有常量(public static final类型)。 |
访问修饰符 | 抽象方法可以是:public、protected | 抽象方法只能是public。 默认为public abstract |
main方法 | 可以有main方法并且我们可以运行它。 | 没有main方法,因此我们不能运行它。 |
设计理念 | 被继承体现的是:”is a”的关系。 抽象类中定义的是该继承体系的共性功能。 | 被实现体现的是:”like a”的关系。 接口中定义的是该继承体系的扩展功能。 |
一些抽象的区别:
接口的设计目的,是对类的行为进行约束(更准确的说是一种“有”约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。
抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)。
抽象类是对类本质的抽象,表达的是is-a的关系,比如:Bnw is-a car。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。
接口是对行为的抽象,表达的是 like-a的关系。比如:Bird like-a Aircraft(像飞行器一样可以飞),但其本质上 is-a Bird。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。
使用场景:
当你关注一个事物的本质的时候,用抽象类;
当你关注一个操作的时候,用接口;
抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为对Java来说(从实际设计上来说也是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口,在设计阶段会降低难度。
抽象类不能实例化为什么有构造方法
1、Java规定类都要有一个构造方法,没有默认提供一个空参构造;
2、构造方法不是用来实例化的,而是用来给属性初始化赋值的,抽象方法可以定义属性,那么就可以用构造方法给属性赋值。这里就可以理解为什么接口没有构造方法了所以属性必须是常量了;
3、抽象方法需要被子类继承,子类的构造方法中用的是super()调用父类的构造方法实例化的,如果抽象类没有构造方法,那么就无法被子类继承了。
不同JDK版本的接口细节
(JDK7及以前版本特点)
接口中的成员变量默认被public,static,final修饰
接口中没有构造方法,实现类中的super访问的是Object的构造方法
接口中所有的成员方法都为抽象方法,默认被public,abstract修饰。因为没有具体的方法体,所以实现类可以实现多个接口
(JDK8及以后版本只对接口中的成员方法做了改进)
1、解决了接口升级问题:
如果接口升级了,定义许多新的抽象方法,那么所有的实现类都需要去重写新的抽象方法,这样十分麻烦。为了解决接口升级的问题,JDK8中允许接口中拥有非抽象方法即默认方法。
默认方法格式:public default 返回值类型 方法名(参数列表) { }
默认方法不是抽象方法,所以在实现类中不强制重写默认方法。在实现类中,默认方法可以被调用和重写,重写的时候去掉default关键字,默认被 public修饰可以省略public。
实现类如果实现了多个接口,并且多个接口中存在相同的默认方法声明,子类就必须对该默认方法进行重写,否则会出现逻辑混乱。
2、引入了静态方法:
静态方法格式:public static 返回值类型 方法名(参数列表) { }
接口中的静态方法只能通过接口名调用,不能通过实现类名或者对象名调用。(所以不同接口中相同的静态方法不用在实现类中重写)
(JDK9中接口的新特性)
Java 8允许在接口中定义带方法体的默认方法和静态方法。这样可能就会带来一个问题:当两个默认方法或者静态方法中包含一段相同的代码实现时,程序必然考虑将这段实现代码抽取成一个共性方法,而这个共性方法是不需要让实现类使用的,因此可以使用private修饰,这就是JDK 9增加的私有方法。
格式1:private 返回值类型 方法名(参数列表) { }
格式2:private static 返回值类型 方法名(参数列表) { }
深拷贝和浅拷贝
(跟设计模式中的原型模式一起学最好)
深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。
1、浅拷贝创建一个新对象,并复制原始对象的所有非静态字段到新对象。但是,如果字段是引用类型,那么只复制引用而不复制引用的对象。因此,对于引用类型的字段,原始对象和新对象共享同一个内部对象。
那么实现浅拷贝呢,方法如下:
我们通常可以通过创建一个新对象,并且使用构造函数或setter方法将原始对象的值复制到新对象中来实现浅拷贝。Java中的自动装箱和拆箱机制可以简化基本数据类型的复制。
2、深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的类执行指向的不是同一个对象。
实现深拷贝的三种方法:
手动实现:通过创建一个新的对象,并逐个复制字段的值。如果字段是引用类型,需要递归地创建该字段的新实例,只不过这个过程比较繁琐。
使用序列化:将对象序列化为字节流,然后再反序列化回一个新对象。这种方法要求对象及其所有组成部分都是可序列化的。
public class DeepCopyViaSerialization {
// 实现深拷贝的方法
public static <T> T deepCopy(T original) {
T copied = null;
try {
// 创建一个字节流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(original);
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()))) {
copied = (T) ois.readObject();
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return copied;
}
}
使用克隆:实现Cloneable接口并重写clone()方法。但这种方法有争议,因为它可能不提供真正的深拷贝,除非所有相关的类都正确实现了clone()方法。
public class User implements Cloneable {
private String name;
private Address address;
@Override
public User clone() throws CloneNotSupportedException {
User user = (User) super.clone();
user.setAddress(this.address.clone());
return user;
}
}
public class Address implements Cloneable {
private String city;
private String country;
@Override
public Address clone() throws CloneNotSupportedException {
return (Address) super.clone();
}
}
==、equals()、hashCode()
Java中==和equals有什么区别
一个是运算符,一个是方法。
==比较变量的值是否相同。
- 如果比较的对象是基本数据类型,则比较数值是否相等;
- 如果比较的是引用数据类型,则比较的是对象的内存地址是否相等。
因为Java只有值传递,对于==来说,不管是比较基本数据类型,还是引用数据类型的变量,其比较的都是值,只是引用类型变量存的值是对象的地址。引用类型对象变量其实是一个引用,它们的值是指向对象所在的内存地址。
equals方法比较对象的内容是否相同。
equals()方法存在于Object类中,而Object类是所有类的父类。在Object类中定义了equals方法:
public boolean equals(Object obj) {
return (this == obj);
}
如果类未重写equals方法,调用equals时,会调用Object中的equals方法(实际使用的也是==操作符)。
如果类重写了equals方法,调用equals时,会调用该类自己的equals方法(一般是比较对象的内容是否相同)。比如:
- String:比较字符串内容是否相同;
- Integer:比较对应的基本数据类型int的值是否相同。
hashCode():hashCode就是对象的散列码,是根据对象的某些信息推导出的一个整数值,默认情况下表示是对象的存储地址。通过散列码,可以提高检索的效率,主要用于在散列存储结构中快速确定对象的存储地址。
hashCode() 的作用是获取哈希码,定义在JDK的Object.java中,Java中的任何类都包含有hashcode() (native)。
hashCode()与equals():
在Java中,每个对象都可以调用自己的hashCode()方法得到自己的哈希值(hashCode),相当于对象的指纹信息,通常来说世界上没有完全相同的两个指纹,但是在Java中做不到这么绝对,但是我们仍然可以利用hashCode来做一些提前的判断,比如:
如果两个对象的hashcode不相同,那么这两个对象肯定不同的两个对象;
如果两个对象的hashCode相同,不代表这两个对象一定是同一个对象,也可能是两个对象;
如果两个对象相等,那么他们的hashCode就一定相同;
为什么重写 equal 要重写 hashCode?
为了保持逻辑上的一致:
Object 中定义的 hashcode 方法生成的哈希码能保证同一个类的对象的哈希码是不同的。当equals 返回为true,我们在逻辑上可以认为是同一个对象,但是査看哈希码,发现哈希码不同,和equals 方法的返回结果违背。
Object 中定义的 hashcode 方法生成的哈希码跟对象的本身属性值是无关的,重写 hashcode 之后,我们可以自定义哈希码的生成规则,可以通过对象的属性值计算出哈希码。
HashMap中,借助equals和hashcode 方法来完成数据的存储:
在HashMap中加入一个元素,首先获取其hashCode再获取hash值,利用hash值将元素定位到哈希表上的位置后——
若该位置为空,则直接插入;若该位置不空,则与该位置上的链表/树上的元素逐一进行equals判断,若无相同元素则插入。
所以如果不重写hashCode的话,本应在逻辑上相等的两个对象就在“定位哈希表索引”一步上就无法到相同位置,从而无法去重。
Java传参机制
Java 是值传递还是引用传递?
Java 的参数传递机制是基于值传递(Pass-by-Value)。
当我们将一个变量作为参数传递给一个方法时,实际上是将这个变量的值传递给了方法内部的形参,而不是将变量本身传递进去。这意味着方法内部对形参的修改不会影响到原始的变量。
但对于引用类型的参数,需要特别注意它们的行为。
在 Java 中,引用类型的变量存储的是对象的引用(内存地址),而不是对象本身。当我们将一个引用类型的变量作为参数传递给一个方法时,实际上是将这个引用的副本传递给了方法内部的形参。因此,方法内部对形参所引用的对象进行的操作,会影响到原始的对象。
如果在方法内部对引用进行了重新赋值操作,即改变了引用所指向的对象,那么这个改变不会影响到原始的引用。这是因为方法内部的引用只是原始引用的一个副本,它们指向的是不同的内存地址。
this、super
this的作用:
用于指代当前对象的引用,访问当前对象的成员变量和方法;
用于区分局部变量和成员变量,当局部变量和成员变量同名时,使用 this 关键字可以明确指定使用成员变量。
用于在构造方法中调用其他构造方法,可以使用 this 关键字来调用同一个类中的其他构造方法。
super的作用:
访问父类中的成员变量或方法,即使子类中有同名的成员变量或方法。
子类的构造方法中,可以使用 super 关键字来调用父类的构造方法,以初始化父类的成员变量。
this只能调用到父类的变量或方法吗?
不对,因为子类继承了父类的变量和方法。
假如子类与父类中没用同名的成员变量或者方法,用 this 关键字就可以去访问到父类还有子类的成员变量或者方法。
但是super并不能调用子类的方法!
区别
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8AVUvwY1-1721242362139)(https://i-blog.csdnimg.cn/direct/55d1a7a55cd04d6984b72ec48e393d28.png)]
修饰符专题
访问修饰符
Java 语言,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。
Java 支持 4 种不同的访问权限:
public : 对所有类可见。使用对象:类、接口、变量、方法。
protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MwDfV8kr-1721242362140)(https://i-blog.csdnimg.cn/direct/89020072827441f8ac316640c3afe418.png)]
非访问修饰符
static 修饰符,用来修饰类方法和类变量。
final 修饰符,用来修饰类、方法和变量,final 修饰的类不能够被继承,修饰的方法不能被继承类重新定义,修饰的变量为常量,是不可修改的。
abstract 修饰符,用来创建抽象类和抽象方法。
synchronized 和 volatile 修饰符,主要用于线程的编程。
transient 修饰符,序列化的对象包含被 transient 修饰的实例变量时,java 虚拟机(JVM)跳过该特定的变量。
static
static用法?
1、修饰成员属性:一般用于定义一些常量。给属性加了static关键字之后,对象就不再拥有该属性了,该属性会由类去管理,即多个对象只对应一个属性。
2、修饰成员方法:static修饰成员方法的作用是可以使用”类名.方法名”的方式操作方法,避免了先要new出对象的繁琐和资源消耗。(静态方法使用场景:当方法不需要访问对象状态,即方法所需信息通过参数传递,或者方法只影响静态变量时。工具类方法,如数学计算函数、文件操作方法等,这些方法不依赖于对象的状态。)
3、修饰代码块:static { }就是静态块,当类加载器载入类的时候,这一部分代码被执行,常用于对静态变量进行初始化工作。(当其他代码用到这个类,类加载器才会将它载入)在静态块中,可以访问静态变量,调用静态方法。
4、修饰内部类:(下面一个问题就是详细描述)
5、静态导包:导入时,使用static关键字,而且在引入类的最后还加上了“.*”,它的作用就是将类中的所有类方法直接导入。不同于非static导入,采用static导入包后,在不与当前类的方法名冲突的情况下,无需使用“类名.方法名”的方法去调用类方法了,直接可以采用”方法名”去调用类方法,就好像是该类自己的方法一样使用即可。
import static com.dotgua.study.PrintHelper.*;
public class Demo{
public static void main( String[] args )
{
//System.out.println(o);
print("Hello World!");
}
}
static不能修饰普通类,但可以修饰内部类。原因如下:
static修饰的东西被我们成为类成员,它会随着类的加载而加载,比如:静态代码块,静态成员,静态方法(这里只是加载,并没有调用)等等。若把一个Class文件中的外部类设为static,那目的何在呢?难道让这个类随着应用的启动而加载吗?如果我在这次使用过程中根本没有使用过这个类,那么是不是就会浪费内存。这样来说设计不合理,总而言之,设计不合理的地方,Java是不会让它存在的。
为什么内部类可以使用static修饰呢,因为内部类算是类的成员了,如果我们没有使用静态来修饰,那么我们在创建内部类的时候就需要先有一个外部类的对象,如果我们一直在使用内部类,那么内存中就会一直存在外部类的引用,而我们有时候只需要使用内部类,不需要外部类,那么还是会浪费内存,甚至会造成内存溢出。使用static修饰内部类之后,内部类在创建对象时就不需要有外部类对象的引用了。
静态变量和实例变量的区别?
静态变量(static变量):
定义:静态变量是类的类变量。它们被所有类的对象共享。
生命周期:静态变量的生命周期与类相同,在类第一次被加载到JVM时创建,在程序结束时销毁。
访问:可以通过类名直接访问静态变量,无需创建类的实例。
用途:静态变量常用于定义类常量和管理类状态。
实例变量:
定义:实例变量是类的非静态字段,每个对象都有自己的一份拷贝。
生命周期:实例变量的生命周期与对象实例相同。当对象被创建时,实例变量被初始化;当对象被垃圾回收时,实例变量被销毁。
访问:实例变量不能通过类名直接访问,必须通过对象实例访问。
用途:实例变量用于存储对象的状态信息。
为什么静态方法不能直接调用非静态变量或方法?
静态方法属于类级别,而非静态变量和方法需要类的实例才能访问。静态方法在没有任何对象实例的情况下可以被调用,因此它不能直接访问属于对象级别的非静态成员。
什么是静态初始化块,它有什么用途?
静态初始化块是一个在类加载时执行的代码块,用于初始化静态变量。它在类加载到JVM时执行,且只执行一次。静态初始化块非常适合执行静态变量的复杂初始化。
单例模式
在单例模式如懒汉式、饿汉式等的实现中,单例对象都被static修饰保证只有一个实例。
main方法为什么是static的
public static void main(String[] args)
1、main方法是给虚拟机调用的;
2、java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是public
3、java虚拟机在执行main()方法时不必创建对象,所以该方法必须是static
4、该方法接收String类型的数组参数,该数组中保存执行iava命令时传递给所运行的类的参数
为什么我们会将工具类的构造器设置为私有的?
将工具类的构造器设置为私有的可以防止外部代码实例化这个类,因为工具类通常只包含静态方法和静态变量。
Java中静态方法可以被重载或重写吗?
静态方法可以被重载,但不能被重写。因为静态方法是属于类的,而不是实例的。重载示例:可以在同一个类中定义多个名称相同但参数不同的静态方法。
static加载顺序(涉及类的加载过程)
涉及类加载过程:(其实是JVM的内容)
加载=> 链接(验证+准备+解析)=> 初始化=> 使用=> 卸载
1、加载(将硬盘上的Java二进制文件(class文件)转为内存中的Class对象)
(1)通过一个类的全限定名获取定义此类的二进制字节流。
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3)在内存(不一定在堆中,HotSpot是在方法区)中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2、链接(给静态变量赋初始值,符号引用替换成直接引用)
(1)验证:检查载入的class文件数据的正确性
(2)准备:给类变量(静态变量)分配内存(方法区)并设置为零值(0、false、nul等)。例外:static final类型的String或基本类型,直接赋值为最终值。
(3)解析(可选):将常量池内的符号引用替换成直接引用。1.符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
3、初始化(初始化类变量(静态变量)、执行静态语句块)
执行类变量(静态变量)的赋值动作和静态语句块(按定义的顺序从上往下执行)。优先级:静态、父类、子类。注意:初始化是操作类变量(也就是静态变量),不是对象的变量。
(五种情况初始化:1、new;2、反射包相关方法对类进行反射调用;3、初始化类时发现其父类未初始化,则先初始化父类;4、虚拟机启动时初始化包含main方法的主类;5、当使用java.lang.invoke.MethodHandle实例进行动态语言支持时,如果方法句柄对应的类没有初始化,则需要先触发其初始化。)
4、使用(以new一个对象为例)
(1)若是第一次创建 Dog 对象(对象所属的类没有加载到内存中)则先执行上面的加载操作。
(2)在堆上为 Dog 对象(包括实例变量)分配空间,所有属性都设成默认值(数字为 0,字符为 null,布尔为 false,引用被设成 null)
(3)初始化实例:给实例变量赋值、执行初始化语句块
(4)执行构造函数检查是否有父类,如果有父类会先调用父类的构造函数
(5)执行本类的构造函数。
关于final
final作用
修饰类:表示类不可被继承。
修饰方法:表示方法不可被子类覆盖,但是可以重载。
修饰变量:表示变量一旦被赋值就不可以更改它的值。
(1)修饰成员变量
如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。
如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值。
(2)修饰局部变量
系统不会为局部变量进行初始化,局部变量必须由程序员显式初始化。因此使用final修饰局部变量时,即可以在定义时指定默认值,或后面的代码中对final变量赋初值(赋值仅能操作一次)。
(3)修饰基本类型数据和引用类型数据
如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改。
如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。但是引用的值是可变的。
为什么局部/匿名内部类只能访问局部final变量?
详解帖子:为什么局部内部类和匿名内部类只能访问 final 的局部变量?
其根本原因就是作用域中变量的生命周期导致的。
首先需要知道的一点是: 内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着方法的执行完毕就被销毁。
这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有没有人再引用它时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解决这个问题,编译器就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的"copy"。这样就好像延长了局部变量的生命周期。
问题又出现了:将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?
答案:就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性。这实际上也是一种妥协。
在Java 8 之后,在内部类或 Lambda 表达式中访问的局部变量,如果不是 final 类型的话,编译器自动加上 final 修饰符,即 Java8 新特性:effectively final。
谈谈final、finally、finalize有什么不同?
(这个问题主要考察以下几个关键点:
语法和定义:了解final、finally、finalize的基本定义和用法。
应用场景:掌握它们在实际编程中的应用场景。
设计目的:理解它们设计的目的和使用中的注意事项。
性能和最佳实践:了解它们在性能和最佳实践方面的影响和推荐使用方式。)
final:
可以用来修饰类、方法和变量,提供更好的代码安全性和可读性。
修饰类,类不能被继承;修饰方法,方法不能被重写;修饰变量,变量值不能被修改。
finally:
finally常与异常捕获相关,try-catch-finally代码块,用于资源释放,如关闭文件流、数据库连接等,确保资源不泄露。
finally块用于保证无论是否抛出异常,都必须执行特定代码。
finalize:
是java.lang.Object类的一个方法,用于在对象被垃圾收集前进行清理操作。
用于清理资源,但不推荐使用。finalize机制在JDK 9中被标记为deprecated,建议使用其他方式进行资源管理,如try-with-resources或Cleaner机制。
finalize为什么被弃用
性能问题:该方法的执行会增加垃圾回收的停顿时间,因为 JVM 必须等待对象的 finalize() 方法执行完毕才能回收对象。这对于需要低延迟和高吞吐量的应用来说可能会产生性能问题
不确定性:执行时间是不确定的,因为它依赖于垃圾回收器的运行时机,而垃圾回收器的运行时机是不可预测的。这导致依赖 finalize() 进行资源清理的代码很难编写和调试
死锁风险:如果 finalize() 方法在执行过程中访问了其他对象,而这些对象又恰好正在被垃圾回收,那么就可能发生死锁
安全问题:可以被恶意代码利用来破坏系统的安全性。例如,恶意代码可以覆盖对象的 finalize() 方法,在对象被垃圾回收时执行恶意操作
因此,基于以上原因,它在 Java 9 中被标记为弃用(deprecated)
Java内存专题
更深入的面试题属于JVM面试题,在另一篇文章中。
对象的创建
对于如下代码
class Person{
int age = 90;
String name;
Person(String n, int a){
name = n;
age = a;
}
}
public class Test{
Person p = new Person("小红", 20);
}
1、创建对象时,首先会将类信息(属性信息、方法信息)加载到方法区。(同一次运行时再创建该对象时不会再加载);
2、再在堆中分配空间,进行默认初始化,age=0,name=null;
3、进行显式初始化 age=90。
4、进行构造器初始化 name=小倩(在常量池),age=20。
5、将堆中的地址付给p,p指向对象;
对象创建时的执行顺序
静态代码块是在类加载时被执行,不管类加载多少次,静态代码块都只会执行一次,第一次加载类的时候执行
· 静态代码块只能调用静态成员
普通代码块是在创建对象时被执行,每创建一个对象就执行一次非静态代码块
· 代码块可以视为构造器的补充,将多个构造器里面共有的部分提取出来,减少代码冗余
· 普通代码块可以调用静态和非静态成员
执行顺序:
(1) 父类的静态属性初始化和静态代码块
(2) 子类的静态属性初始化和静态代码块
(3) 父类普通属性初始化和普通代码块
(4) 父类构造器显示代码
(5) 子类普通属性初始化和普通代码块
(6) 子类构造器显示代码
类什么时候会被加载
类什么时候被加载
创建对象实例时(new);
创建子类对象实例,父类也会被加载;
使用类的静态成员时(静态属性,静态方法) 。
基本数据类型及其包装类专题
java 是一个完全面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是Integer,从 Java 5开始引入了 自动装箱/拆箱机制,使得二者可以相互转换。
Long或Integer如何比较大小?
错误方法:
1、使用==。 因为Long与Ineger都是包装类型,是对象。 而不是普通类型long与int
2、使用equals方法。因为equals方法只能比较同类型的类,例如两个都是Integer类型。
正确方法:
先使用longValue()或intValue()方法来得到他们的基本类型的值然后使用==比较也是可以的。
基本数据类型(及包装类型)都有哪些各占几个字节?
基本数据类型 | 包装类型 | 所占的字节数 |
---|---|---|
byte | Byte | 1 |
short | Short | 2 |
int | Integer | 4 |
long | Long | 8 |
float | Float | 4 |
double | Double | 8 |
char | Charater | 2 |
boolean | Boolean | 1 |
拆箱与装箱原理
装箱就是将基本数据类型转化为包装类型,那么拆箱就是将包装类型转化为基本数据类型。
public class Demo{
public static void main(String[] args) {
//自动装箱,底层其实执行了Integer a=Integer.valueOf(10);
Integer a = 10;
//自动拆箱,底层其实执行了int b=a.intValue();
int b = a;
}
}
生成并查看反汇编文件,依次执行如下命令:
javac -d . org\example\a\Demo.java
javap -c org.example.a.Demo
E:\work\idea_proj\test_java\src>javap -c org.example.a.Demo
Compiled from “Demo.java”
public class org.example.a.Demo {
public org.example.a.Demo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object.”<init>”:()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 10
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: astore_1
6: aload_1
7: invokevirtual #3 // Method java/lang/Integer.intValue:()I
10: istore_2
11: return
}
以Integer为例,当Integer 对象赋一个 int 值的时候,会调用 Integer 类的静态方法valueOf。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
IntegerCache 是 Integer 的内部类,如果整型字面量的值在-128到127之间,那么不会 new 新的 Integer 对象,而是直接引用常量池中的Integer对象。
基本类型与包装类的比较
Integer是int的封装类,当Integer与int进行==比较时,Integer就会拆箱成一个int类型,所以还是相当于两个int类型进行比较,这里的Integer不管是直接赋值,还是new创建的对象,只要跟int比较就会拆箱为int类型,所以就是相等的。
public class Demo {
public static void main(String[] args) {
int int1 = 12;
int int2 = 12;
Integer integer1 = new Integer(12);
Integer integer2 = new Integer(12);
System.out.println("int1 == int2 : " + (int1 == int2)); //true
System.out.println("int1 == integer1 : " + (int1 == integer1)); //true
System.out.println("integer1 == integer2 : " + (integer1 == integer2)); //false
}
}
下面代码报错空指针异常,因为基本类型与包装类型进行比较,此时会将包装类型自动拆箱,调用a.intValue()获得其基本类型的值,但a是null,所以报空指针异常。
public class Demo {
public static void main(String[] args) {
Object object = true;
System.out.println(object);
boolean b = (boolean)object;
System.out.println(b);
System.out.println(b == true);
}
}
缓存:当i的值位于[-128,127]的时候,会直接返回Integer缓存数组中相应对象的引用,如果i大于127或小于-128,会重新创建一个Integer实例,并返回。
public class Demo{
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
System.out.println(a == b);//true
System.out.println(c == d);//false
}
}
其他包装类的缓存
包装类 | 说明 |
---|---|
Byte | 相同值的Byte比较永远返回true。因为byte取值范围就是[-128,127]。 |
Short、Integer、Long | 相同值在[-128,127]则返回true,不在则返回false |
Character | 要返回true,只需保证i <= 127。因为char最小值为0,本来就大于等于-128。 |
Float、Double | 永远返回false。因为其永远返回新创建的对象,因为一个范围内的整数是有限的,但是小数却是无限的,无法保存在缓存中。 |
Boolean | 只有两个对象,要么是true的Boolean,要么是false的Boolean,只要boolean的值相同,Boolean就相等。 |
short s1 = 1; s1 = s1 + 1; 有错吗?short s1 = 1; s1 += 1; 有错吗
前者错!后者对!
对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int 型,需要强制转换类型才能赋值给 short 型。
而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1; 相当于 s1 = (short)(s1 + 1); 其中有隐含的强制类型转换。
Integer和int的区别在哪,为什么需要设计封装类?
区别:
1、integer是基本数据类型,int的封装类在java里面有八种基本数据类型,基本数据类型都有一一对应的封装类型。
2、int类型可以直接定义一个变量名称赋值,Integer需要去使用new关键字来创建对象(当然Java5后可以自动装箱)。
3、Integer作为一个对象类型封装了一些方法和属性,可以利用这些方法来操作数据。
4、作为成员变量,Integer的默认值是null,而int的默认值呢是零。
……
之所以要对基础类型设计一个对应的封装类型,是因为:
1、基本数据类型方便、简单、高效,但泛型不支持、集合元素不支持
2、不符合面向对象思维
3、包装类提供很多方法,方便使用,如 Integer 类 toHexString(int i)、parseInt(String s) 方法
4、还有很多好处,比如安全性比较好,可以避免外部操作随意修改成员变量值,保证了成员变量和数据传递的安全性,隐藏了实现细节,对使用者更加友好,只需要去调用对象提供的方法,就可以完成对应的操作。
建议回答:
Integer和int的区别有很多,我简单罗列三个方面:第一个作为成员变量来说,Integer的初始值是null的,int的初始值是零;第二个Integer是存储在堆内存中,因为它是一个对象,而int类型它是直接存储在栈空间中;第三个Integer是一个对象类型,它封装了很多的方法和属性,我们在使用的时候呢会更加灵活。
为什么要设计成封装类型,我认为主要的原因是java本身是一个面向对象的语言,一切操作都是以对象作为基础的,比如说像集合里面存的元素,也只支持存储Object类型,普通类型是无法通过集合来存储的。
String专题
package java.lang;
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
// 其他代码
}
String不可变的含义、原因、好处
含义:不可变的含义是内部数据不可变,而不是说引用不可变。
原因:String的内部数据是一个char数组,是对字符串数组的封装,并且是被final修饰的,创建后不可改变
好处:
1、便于实现字符串池(String pool):在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。如果字符串是可变的,某一个字符串变量改变了其值,那么其指向的变量的值也会改变,String pool将不能够实现!
2、使多线程安全:在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。
3、避免安全问题:在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。
因为String是不可变的,所以它的值是不可改变的。但由于String不可变,也就没有任何方式能修改字符串的值,每一次修改都将产生新的字符串,如果使用char[]来保存密码,仍然能够将其中所有的元素设置为空和清零,也不会被放入字符串缓存池中,用字符串数组来保存密码会更好。
4、加快字符串处理速度:由于String是不可变的,保证了hashCode的唯一性,于是在创建对象时其hashCode就可以放心的缓存了,不需要重新计算。这也就是一般将String作为Map的Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。在String类的定义中private int hash属性缓存hashcode。(它们的 hashcode 在创建时就被缓存了,不需要重新计算。这就使得字符串很适合作为 Map 中的键。)
为什么String类是final的?
1、主要是为了“效率”和“安全性”的缘故。若 Sting允许被继承,由于它的高度被使用率,可能会降低程序的性能。
2、String类被设计为final类是为了防止恶意代码的注入。String类内部使用了本地方法调用,如果String类可以被继承并修改,那么可能会被注入恶意代码,从而引发安全问题。
3、而且String类的不可变性带来了许多好处。不可变性使得String对象在创建后不能被修改,这保证了线程安全,多个线程可以安全地共享String对象而不会出现数据不一致的问题。此外,不可变性还提高了缓存效率,JVM可以利用字符串常量池来复用相同的字符串对象,减少内存消耗。
String的字符数据可变
String类使用char value[]来存字符数据,它的类型为:private final char value[];(final只是表示不能指向其他地址,它里边的内容是可以更改)。
结论:String是可以更改的,使用反射,value.setAccessible(true),然后修改它即可。
String、StringBuffer、StringBuilder
项 | String | StringBuffer | StringBuilder |
---|---|---|---|
可变性 | 不可变。原因:value数组是final类型。因为不可变,所以每次操作生成新对象。 | 可变。原因:其父类(AbstractStringBuilder)的value数组不是final类型 | 可变。原因:其父类(AbstractStringBuilder)的value数组不是final类型 |
线程安全性 | 线程安全。原因:value数组是final类型 | 线程安全。原因:方法都用了synchronized。(单线程时没必要用,因为加锁了,速度慢。) | 线程不安全。(单线程时建议使用,因为没加锁,速度快。) |
数据类型之间如何进行转换?
自动类型转换
数据类型按精度大小排序
char —> int —> long —> float —> double
byte —> short —> int —> long —> float —> double
● 当有多种数据混合运算时,系统首先自动将所有数据转换成容量最大的数据类型,再进行计算。
● 当把精度大到数据类型赋给精度小的会报错。注意在进行数值赋值时,先判断是否在该小精度数据类型范围内,如果是就可以,如果是进行变量赋值,就不行。
● byte,short和char之间不能相互自动转换。
● byte,short和char三者可以计算,计算时转换成为int类型。
● boolean类型不参与转换。
● 自动提升原则:表达式结果的类型自动转换成操作数中最大的类型。
强制类型转换
自动类型转换的逆过程,将容量大的数据类型装换成容量小的数据类型。使用时要加上强制转换符,但可能造成精度降低或溢出。
● 强制类型转换只对最近的操作数有效,往往会使用小括号提升优先级。
● char类型可以保存int的常量值,但不能保存int的变量值,需要强转。
基本数据类型与String类型的转换
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2bdFvWtu-1721242362147)(https://i-blog.csdnimg.cn/direct/7859eef130984b30bb5dbd64333469ab.png)]
String创建对象的数量
1、字面量+字面量
如:
String s1 = “abc” + “def”;
答案:1个。
编译期已经常量拼为"abcdef",放到常量池,变量s1获得是"abcdef"。
2、字面量+对象+字面量
如:
String s1 = “abc”;
String s2 =“abc”+s1+“def”;
答案:创建3个对象。常量池中2个:abc,def;堆中1个:abcabcdef
解析:
String s1 = “abc”;:创建1个对象:生成了一个String对象"abc"并放入常量池(其中的字符串池)中,定义了一个String引用变量s1并指向"abc"。
String s2 =“abc”+s1+“def”;创建2个对象:"abc"已经在池中了,直接从池中取出;s1是引用地址,即:s1=="abc"为true;创建了一个"def"的String对象并放入池中。创建一个"abcabcdef"的String对象存放于堆(而不是常量池)。
package org.example.a;
public class Demo {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);//true
String s3 = "abc" + s1 + "def";
String s4 = "abcabcdef";
System.out.println(s3 == s4);//false
String s5 = s3.intern();
System.out.println(s4 == s5);//true
}
}
3、new String(“xx”) + new String(“xx”)
如:
String s = new String(“abc”) + new String(“abc”);
答案:创建4个String对象。
解析:
JVM先在常量池中创建1个String对象存储abc;
遇到new关键字,再在Java堆上创建1个String对象,其char value[]则指向常量池中的char value[];
常量池中已有abc的对象,所以第二个new语句只在内存堆上创建1个String对象,其char value[]则指向常量池中的char value[];
两个字符串相加会在堆上创建1个String对象"abcabc"。(因为没有显式使用双引号指定,也没有调用intern,所以常量池里边目前没有“abcabc”对象)
4、字面量+new String(“xx”)
如:
String s = “abc” + new String(“def”);
答案:4个
解析:
JVM先在字符串常量池中创建2个String对象存储abc和def;
遇到new关键字,再在内存堆上创建1个String对象,其char value[]则指向常量池中的def;
两个字符串相加会在堆上创建1个String对象abcdef。(因为没有显式使用双引号指定,也没有调用intern,所以字符串池里边目前没有“abcdef”对象)
String类的intern()方法
在 JAVA 语言中有8种基本类型和一种比较特殊的类型String。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池(在方法区)的概念。常量池就类似一个JAVA系统级别提供的缓存。
8种基本类型的常量池都是系统协调的,String类型的常量池比较特殊。String的常量池的主要使用方法有两种:
- 直接使用双引号声明出来的String对象会直接存储在常量池中。
- 如果不是用双引号声明的String对象,可以使用String提供的intern方法将其放到常量池。
intern方法简介(JDK7之后)
public native String intern();
说明:从字符串常量池中查询当前字符串是否存在(通过equals判断)。
- 如果存在,返回常量池中的字符串引用。
- 如果不存在,把这个String对象引用存到常量池,然后返回这个String对象的引用。
返回值:都是返回String变量对应的常量池中字符串的引用。
原理(JDK6/7后)
常量池里的字符串的由来:
JDK6及以前调用String.intern()
- 若常量池中有,则返回常量池中这个字符串的引用
- 若常量池中没有,则拷贝一份对象,放到常量池(永久代)中;返回值是常量池(永久代)中对应字符串实例的引用。
JDK7及以后调用String.intern()
- 若常量池中有,则返回常量池中这个字符串的引用
- 若常量池中没有,则拷贝一份引用,放到常量池(堆)中;(JDK1.7将String常量池从Perm区移动到了Java Heap区)
package com.example.a;
public class Demo {
public static void main(String argv[]) {
String s1 = new String("1");
s1.intern();
String s2 = "1";
System.out.println(s1 == s2);//JDK6,JDK7分别为false,false
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);//JDK6,JDK7分别为false,true
//JDK6调用intern,若常量池中没有,则拷贝一份对象,放到常量池(永久代)中,返回值是常量池(永久代)中对应字符串实例的引用。
//JDK7后调用intern,若常量池中没有,则拷贝一份引用,放到常量池(堆)中;
}
}
package com.example.a;
public class Demo {
public static void main(String argv[]) {
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();//JDK7发现常量池已有11,返回常量池中这个字符串的引用
System.out.println(s3 == s4);//JDK6,JDK7分别为false,false
}
}
如何保证变量s指向的是字符串常量池中的数据呢?
1、如果是字面量直接赋值引用,则指向字符串常量池:String s = “abc”;
2、或者通过intern():str.intern();
String 转换成 Integer的方式及原理
详细分析:String转换Integer原理
Integer.parseInt()
该方法的原理是遍历字符串中的每个字符,并将其转换为对应的数字。
1)parseInt(String s)内部调用parseInt(s, 10)默认为10 进制。
2)正常判断null\进制范围,length 等。
3)判断第一个字符是否是符号位。
4)循环遍历确定每个字符的十进制值。
5)通过*=和-=进行计算拼接。
6)判断是否为负值返回结果。
字符串拼接
五种方法:
- 加号 “+”
- String contact() 方法
- StringUtils.join() 方法
- StringBuffer append() 方法
- StringBuilder append() 方法
使用+字符串拼接的问题
参考:Java String 字符串拼接的三种方式与效率对比分析
如果在for循环里不断的使用“+”拼接字符串:
String a = "";
for(;;){
a += "123";
//反编译后:a = new StringBuilder().append("123").toString();
//会产生大量临时的StringBuilder对象,这样对性能不好
//而且StringBuilder对象的toString方法本质上也是new String
}
应避免在循环体中使用加号拼接字符串,原因:
1、每次循环都会创建一个新的StringBuilder对象;
2、String对象是不可变的,每次连接操作都会生成一个新的String对象。
可能会导致的问题:
1、内存泄漏OutOfMemoryError:代码中存在未及时释放的对象引用,导致垃圾回收器无法回收这些对象。
2、对象创建太多:频繁创建大量临时对象会增加垃圾回收的压力
因此,这样增加了内存开销和性能损耗。
性能比较
java字符串拼接_Java 字符串拼接 五种方法的性能比较分析
序列化和反序列化
参考文章:Java面试基础——序列化和反序列化
什么是序列化?什么是反序列化?
序列化:将数据结构或对象转换成二进制字节流的过程
反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
常见应用场景?
对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
序列化协议对应于 TCP/IP 4 层模型的哪一层?
OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据,对应的就是序列化和反序列化。
而OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。
如果有些字段不想进行序列化怎么办?transient
对于不想进行序列化的变量,使用 transient 关键字修饰。
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
关于 还有几点注意:
· transient 只能修饰变量,不能修饰类和方法。
· transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0。
· static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。
对象流
ObiectOutputStream 提供序列化功能
ObjectInputStream 提供反序列化功能
同样是包装流,且用修饰器模式,包装InputStream和OutputStream。
序列化与反序列化注意事项:
读写顺序要一致;
要求实现序列化或反序列化对象,需要 实现 Serializable;
序列化的类中建议添加SerialVersionUlD,为了提高版本的兼容性;
序列化对象时,默认将里面所有属性都进行序列化,但除了static或transient修饰的成员;
序列化对象时,要求里面属性的类型也需要实现序列化接口;
序列化具备可继承性,也就是如果某类已经实现了序列化,则它的所有子类也已经默认实现了序列化。
public class ObjectOutputStream_ {
public static void main(String[] args) {
//注意,序列化后生成的文件并不是纯文本,而是特殊的一种数据格式
String filePath = "d:\\a.dat";
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(filePath));
oos.writeInt(100); //int->Integer(实现了 Serializable)
oos.writeBoolean(true); //boolean->Boolean(实现了Serializable)
oos.writeChar('a'); //char->Character(实现了 Serializable)
oos.writeDouble(9.5); //double->Double(实现了 Serializable)
oos.writeUTF("韩顺平教育"); //string
oos.writeObject(new Dog("小狗", 3));
//如果Dog没有实现Serializable接口,会抛出异常NotSerializableException
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(oos != null) oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class ObjectInputStream_ {
public static void main(String[] args) {
String srcPath = "d:\\a.dat";
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(srcPath));
//读取(反序列化)的顺序需要和你保存数据(序列化)的顺序一致
//否则会出现异常
System.out.println(ois.readInt());
System.out.println(ois.readBoolean());
System.out.println(ois.readChar());
System.out.println(ois.readDouble());
System.out.println(ois.readUTF());
//底层Object -> Dog
Object dog = ois.readObject();
//dog的编译类型是Object,运行类型是Dog
System.out.println(dog.getClass());
System.out.println(dog);
//输出结果:
//class cn.io.object.Dog
//Dog{name='小狗', age=3}
//重要细节:
//如果我们需要调用Dog的方法,需要向下转型
//如果Dog是public类那最简单了,直接向下转型即可
//否则将Dog类的定义拷贝到可以引用的位置即可
((Dog) dog).bark();
//注意:修改了Dog类需要重新序列化,再反序列化,否则会抛出异常InvalidClassException
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
关于serialVersionUID
如果定义了一个类,序列化后,若修改了该类,则需要重新序列化再反序列化,否则会抛出异常InvalidClassException。(在序列化一个文件后,新加一个属性,但不指定序列号,此时反序列化,报错InvalidClassException,提示当前Dog类版本号与文件内的版本号不一致)
因为若不指定serialVersionUID,实现了Serializable在修改后会重新生成一个serialVersionUID,这就导致了反序列化时文件内的serialVersionUID与修改后的类的serialVersionUID不一致,所以会报异常,反序列化失败。
所以对于常修改的类,指定serialVersionUID版本号,提高兼容性。
如果从始至终指定一个版本号 1L,那么反序列化,程序读取序列化文件时,忽略更新的属性和方法,不报错。加了序列号之后,只会认为修改后的类是原先类的一个升级版或者修改版,在反序列化时不会因为修改后的类是新的类而报错。
private static final long serialVersionUID = 1L;
内部类专题
不太常见到这部分的面试题。
Java为什么用内部类
提供一种方法把只在一个地方用的类逻辑分组
增强封装(可以理解一种类似“多重继承”)
容易维护
为什么说内部类可以实现类似的多重继承,Java是不需要class多重继承的, 想要实现类似多重继承的效果可以用内部类,比如Class B 已经继承了Class C 就没有办法继承Class A, 但是把B设计成A的内部类,就可以访问A的内部变量和方法了,就像是也继承了A。
一个类的内部又完整的嵌套了另一个类结构,被嵌套的类称为内部类,嵌套其他类的类称为外部类。
内部类是类的第五大成员。(属性、方法、构造器、代码块、内部类)
没有嵌套和被嵌套关系的类称为 外部其他类。
内部类的分类:共四种,局部内部类、匿名内部类、成员内部类、静态内部类
1、定义在外部类的局部位置上(比如某个方法中):
(1)局部内部类(有类名)
- 可以直接访问外部类的所有成员,包含私有的;
- 不能添加访问修饰符,因为它的地位就是一个局部变量。但是可以使用final修饰;
- 作用域:仅仅在定义它的方法或代码块中
- 局部内部类 访问 外部类的成员:直接访问
- 外部类 访问 局部内部类的成员:在作用域内创建对象再访问(必须在作用域内)
- 外部其他类 不能访问 局部内部类(因为局部内部类地位和局部变量一样)
- 如果外部类 和局部内部类的成员重名时,遵循就近原则。如果想访问外部类的成员,可以使用 外部类名.this.成员。
public class localInnerClass {
public static void main(String[] args) {
//创建外部类的对象并调用 m1方法
Outer outer = new Outer();
outer.m1();
/** 调用m1后的运行过程
* 创建 Inner 类 -> 发现内部需要创建 Inner对象 -> 创建对象
* -> 调用 f1()-> 调用 m2()
*/
//用Hashcode 检验 outer 和 Outer.this是否为同一个对象
System.out.println("outer对象 的Hashcode:" + outer);
}
}
class Outer {
private int n1 = 100;//私有属性
private void m2(){
System.out.println("Outer 的 m2()方法");
};//私有方法
public void m1(){//局部内部类通常定义在方法中
final class Inner{//2. 不能添加修饰符但可以用final修饰
private int n1 = 800;//内部类中定义一个和外部类属性同名的属性n1
public void f1(){//1. 可以直接访问外部类的包括私有在内的所有成员
System.out.println("n1=" + n1);//4. 访问外部类方式:直接访问。但是第7点内部类定义了一个重名的属性,此处遵循就近原则变为访问内部类的n1
System.out.println("外部类的n1:" + Outer.this.n1);//外部类的n1改成用 外部类名.this.n1 来访问
/** 解释为什么用Outer.this:
* 1. 存在访问冲突时,需要准确找到属性
* 2. Outer.this 本质上是Outer类的对象,即哪个对象调用了 m1, 哪个对象就是 Outer.this
* 3. 主函数创建了 Outer对象 outer, 并调用了m1, 所以outer就是这里的 Outer.this
*/
m2();
System.out.println("Outer.this 的Hashcode:" + Outer.this);
}
}
/** 如果用final修饰了class Inner,那么就不能在方法中再继承Inner类了
* class Inner02 extends Inner { }
*/
//5. 外部类访问内部类方式:在作用域中,通过创建内部类对象来访问成员
//创建在方法外则编译器找不到内部类在哪
Inner inner = new Inner();
inner.f1();
}
{//3. 作用域为定义它的方法或代码块中
class Inner03 {}
}
}
输出:
n1=800
外部类的n1:100
Outer 的 m2()方法
Outer.this 的Hashcode:com.learnJava.innerClass.Outer@6d03e736
outer对象 的Hashcode:com.learnJava.innerClass.Outer@6d03e736
(2)匿名内部类(没有类名)
基本语法:
new 类或接口(参数列表){
类体
};
和局部内部类相似处:
1、不能添加访问修饰符,因为它地位等同于局部变量
2、作用域:仅仅在定义它的方法或代码块中
3、匿名内部类 直接访问 包括私有在内 的外部类成员
4、外部其他类 不能访问 匿名内部类
5、如果外部类 和局部内部类的成员重名时,遵循就近原则。
如果想访问外部类的成员,可以使用 外部类名.this.成员 去访问
特点:
匿名内部类既是一个类的定义,同时本身也是一个对象。
实践
匿名内部类可以当做实参直接传递。创建的对象只使用一次时,会更方便。(表现在代码上即:函数的括号中直接new对象出来)
分别为基于接口 与 基于类的:
public class AnonymousInnerClass {
public static void main(String[] args) {
Outer02 outer02 = new Outer02();
outer02.method();
}
}
class Outer02 {
private int n1 = 10;
public void method(){//方法
/** 一、基于接口的匿名内部类
* 需求:使用IA接口创建对象
* 1. 传统方式:写一个类实现该接口并创建对象
* 2. 匿名方式: 如果一个类只使用一次,则可以选择用匿名方式
*/
IA tiger1 = new Tiger();//传统方式:创建类再创建对象
tiger1.cry();
//匿名方式:不能直接创建接口对象,但是可以后面跟着实现该接口,而不用单独创建一个类
IA tiger2 = new IA(){//编译类型:IA, 运行类型:匿名内部类
@Override
public void cry() {
System.out.println("匿名-老虎叫");
}
};//记得加分号
tiger2.cry();//类虽然不能再次创建对象了,但它生成的唯一实例可以反复使用
tiger2.cry();
System.out.println("tiger2的运行类型:" + tiger2.getClass());//用来查看匿名内部类返回的类型,输出class com.learnJava.innerClass.Outer02$1也就是说生成的名字是外部类名$数字,数字从1开始
/**
* 匿名内部类底层实现方式也相当于创建了一个类来实现接口,创建完后把地址返回给tiger2
* 但是对于开发者而言效率更高了
* class Outer02$1 implements IA(){
* @Override
* public void cry() {
* System.out.println("匿名-老虎叫");
* }
* }
*
*/
/*
二、基于类的匿名内部类
*/
Father father = new Father("jack"){//此处 father的编译类型是 Father, 但运行类型不是 Father,而是 匿名内部类
@Override
public void test() {
System.out.println("匿名内部类重写了Father类中的test()");
}
};
System.out.println("father 的运行类型" + father.getClass());
/*
* 类的匿名内部类底层实现:
* class Outer02$2 extends Father(){
* @Override
* public void test() {
* System.out.println("匿名内部类重写了Father类中的test()");
* }
* }
*/
}
}
interface IA{//接口
public void cry();
}
class Tiger implements IA{//传统方式
@Override
public void cry() {
System.out.println("对象-老虎叫");
}
}
class Father{//类
public Father(String name) {//构造器
}
public void test(){//方法
}
}
输出:
对象-老虎叫
匿名-老虎叫
tiger2的运行类型:class com.learnJava.innerClass.Outer02$1
father 的运行类型class com.learnJava.innerClass.Outer02$2
2、定义在外部类的成员位置上:
(1)成员内部类(没有static修饰的类)
成员内部类是定义在外部类的成员位置的,且没有static修饰。
- 可以直接访问外部类的所有成员
- 可以添加任意访问修饰符,也即地位和成员相同
- 作用域:整个外部类体中都可使用
- 成员内部类 直接访问 外部类
- 外部类 创建对象再访问 内部类
- 外部其他类 三种访问 内部类
- 如果外部类 和局部内部类的成员重名时,遵循就近原则。
- 如果想访问外部类的成员,可以使用 外部类名.this.成员 去访问
public class MemberInnerClass {
public static void main(String[] args) {
Outer08 outer08 = new Outer08();
System.out.println("外部类调用结果:");
outer08.t1();//外部类通过调用方法来使用成员内部类
System.out.println("\n外部其他类调用结果:");
//外部其他类如何创建内部类?(注意这里是外部其他类,不是外部类)
//1. 通过创建对象来new一个
Outer08.Inner08 inner08 = outer08.new Inner08();
inner08.say();
//2. 在外部类编写一个方法,用来返回内部类
Outer08.Inner08 inner08instance = outer08.getInner08Instance();
inner08instance.say();
//3. 将1和2合起来,熟练之后可以用
}
}
class Outer08{
private int n1 = 10;
public String name = "张三";
//以下成员内部类定义在和属性、方法平级的位置上
class Inner08 {
public void say(){
//可以直接访问外部类的所有成员
System.out.println("n1=" + n1 + " name=" + name);
}
}
//写一个方法用来返回Inner08实例,对应主函数中的第2点
public Inner08 getInner08Instance(){
return new Inner08();
}
//写一个方法来使用成员内部类
public void t1(){
Inner08 inner08 = new Inner08();
inner08.say();
}
}
输出结果:
外部类调用结果:
n1=10 name=张三
外部其他类调用结果:
n1=10 name=张三
n1=10 name=张三
n1=10 name=张三
(2)静态内部类(有static修饰的类)
静态内部类是定义在外部类的成员位置的,有static修饰。
可以直接访问外部类的所有静态成员,但不能直接访问非静态成员
可以添加任意访问修饰符
作用域为整个类
访问方式和成员内部类相似,但要加上静态成员相关的限制
静态内部类—访问---->外部类(比如:静态属性)[访问方式:直接访问所有静
态成员]
外部类—访问-----》静态内部类 访问方式:创建对象,再访问
外部其他类—访问----->静态内部类
如果外部类和静态内部类的成员重名时,静态内部类访问的时,默认遵循就近
原则,如果想访问外部类的成员,则可以使用(外部类名.成员)去访问
内部类持有外部类会导致内存泄露
非静态内部类会持有外部类,如果有地方引用了这个非静态内部类,会导致外部类也被引用,垃圾回收时无法回收这个外部类(即使外部类已经没有其他地方在使用了)。
解决方案
不要让其他的地方持有这个非静态内部类的引用,直接在这个非静态内部类执行业务。
将非静态内部类改为静态内部类。内部类改为静态的之后,它所引用的对象或属性也必须是静态的,所以静态内部类无法获得外部对象的引用,只能从 JVM 的 Method Area(方法区)获取到static类型的引用。
内存泄露实例:
class Outer{
private int[] data;
public Outer(int size) {
this.data = new int[size];
}
class Innner{
}
Innner createInner() {
return new Innner();
}
}
public class Demo {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
int counter = 0;
while (true) { //运行了八千多次的时候就内存溢出了。
list.add(new Outer(100000).createInner());
System.out.println(counter++);
}
}
}
内部类持有外部类引用
匿名内部类为什么要持有外部类
Java 语言中,非静态内部类的主要作用有两个:
当匿名内部类只在外部类(主类)中使用时,匿名内部类可以让外部不知道它的存在,从而减少了代码的维护工作。
当匿名内部类持有外部类时,它就可以直接使用外部类中的变量了,这样可以很方便的完成调用。
普通内部类持有外部类引用原理
内部类虽然和外部类写在同一个文件中, 但是编译完成后, 还是生成各自的class文件,内部类通过this访问外部类的成员。
1、编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同, 这个成员变量就是指向外部类对象(this)的引用;
2、编译器自动为内部类的构造方法添加一个参数, 参数的类型是外部类的类型, 在构造方法内部使用这个参数为内部类中添加的成员变量赋值;
3、在调用内部类的构造函数初始化内部类对象时,会默认传入外部类的引用。
匿名内部类持有外部类导致内存泄露的原因和解决方案
Java 语言中,非静态内部类的主要作用有两个:
当匿名内部类只在外部类(主类)中使用时,匿名内部类可以让外部不知道它的存在,从而减少了代码的维护工作。
当匿名内部类持有外部类时,它就可以直接使用外部类中的变量了,这样可以很方便的完成调用,如下代码所示:
什么时候会内存泄露
非静态方法返回匿名内部类的引用可能导致内存泄露,例:
class Test{
public List createList() {
List list = new ArrayList() {{
add(“a”);
add(“b”);
}};
return list;
}
}
跟上边“普通内部类” 一样,若Test类里边有比较大的对象,而这些大对象根本没被用到,则会内存泄露。
如何避免在 Java 中使用双括号初始化
尽管使用 Java 的双括号初始化看起来很"炫酷",但它会无故地额外创建类,可能会导致内存泄漏。因此避免在 Java 中使用双括号初始化。
void innerClassMethod(String ticketId) {
Map<String, Object> metadata = new HashMap<String, Object>() {{
put("ticketId", ticketId);
}};
}
实际上被编译为:
class OuterClass$1 extends HashMap<String, Object> {
private final OuterClass this$1;
OuterClass$1(OuterClass this$1, String ticketId) {
this.this$1 = this$1;
put("ticketId", ticketId);
}
}
void innerClassMethod(String ticketId) {
Map<String, Object> metadata = new OuterClass$1(this, ticketId);
}
枚举
枚举是一组常量的集合,简写为enum
枚举属于特殊的类,里面只包含一组有限特定的对象
使用enum关键字开发一个枚举类时,默认会继承Enum类
使用简化语法时,必须明确调佣的是哪个构造器
使用无参构造器创建枚举对象时,实参列表 和 小括号 都可以省略
public class enumeration02 {
public static void main(String[] args) {
System.out.println(Season02.SPRING);
}
}
//使用enum实现
enum Season02{
/** 步骤
* 1. 使用关键字enum 代替 class
* 2. 用 常量名(实参列表) 代替创建对象的语法
* SPRING("春天","温暖"); 本质上等价于 public static final Season SPRING = new Season("春天","温暖");
* 3. 有多个常量,使用 , 号分隔
* 4. enum类中,定义常量的语句写在最前面
*/
SPRING("春天","温暖"),SUMMER("夏天","炎热"),AUTUMN("秋天","凉爽"),WINTER("冬天","寒冷");
private String name;
private String desc;//描述
private Season02(String name, String desc) {
this.name = name;
this.desc = desc;
}
public String getName() {
return name;
}
public String getDesc() {
return desc;
}
@Override
public String toString() {
return "Season{" +
"name='" + name + '\'' +
", desc='" + desc + '\'' +
'}';
}
}
对该枚举类进行反编译,发现它自动继承Enum类,拥有values, valueOf, toString方法。
下面的代码是否正确?并说明含义.
enum Gender{
BOY,GIRL;
}
语法正确,上面是一个枚举类Gender
该枚举类没有属性,也只有默认无参构造器,因此常量名后面不需要括号(加也可以,但没必要)
下面的代码输出什么?
enum Gender2{
BOY,GIRL;
}
Gender2 boy = Gender2.BOY;
Gender2 boy2 = Gender2.BOY;
System.out.println(boy); //BOY
System.out.println(boy == boy2); //true
//详解如下
public class enumExercise01 {
public static void main(String[] args) {
Gender2 boy = Gender2.BOY;
Gender2 boy2 = Gender2.BOY;
System.out.println(boy);//输出boy所在类的toString方法,没有就用父类Enum的toString方法。
/**
Enum类的toString 底层实现如下, name为类名
public String toString(){
return name;
}
*/
System.out.println(boy == boy2);//本质上是同一个枚举对象true
}
}
枚举对象的常用方法
toString: Enum已经重写过,返回当前对象名。子类可以重写用于返回对象的属性信息。Enum类中建议“最好用toString获取枚举常量名“
name:返回当前对象名,子类中不能重写。
ordinal:返回当前对象的位置号,默认从0开始
values: 返回当前枚举类中的所有常量
valueOf: 将字符串转换成枚举对象,要求字符串必须为已有的常量名否则报错。
compareTo: 比较两个枚举常量,比较的就是位置号
注意事项
使用enum后:
不能再继承其他类,因为java是单继承,而使用enum关键字会默认继承了Enum类
可以实现接口。如下:
enum 类名 implements 接口1, 接口2{}
如果枚举类无实例,也必须在第一行留下“;“
public class EnumTest {
public static void main(String[] args) {
for(Gender gender : Gender.values()){
System.out.println("================");
System.out.println("gender.name(): " + gender.name());
System.out.println("gender.getName(): " + gender.getName());
System.out.println("gender.toString(): " + gender.toString());
System.out.println("gender: " + gender);
System.out.println("gender.ordinal(): " + gender.ordinal());
}
}
}
public enum Gender{
// 此类用于测试枚举类中其中一个字段为name,
// 即与Enum类中的保存枚举常量名的字段重名时的情况
BOY("男孩", 1), GIRL("女孩", 2);
private String name;
private int code;
private Gender(String name, int code){
this.name = name;
this.code = code;
}
public String getName() {
return name;
}
public int getCode() {
return code;
}
}
================
gender.name(): BOY
gender.getName(): 男孩
gender.toString(): BOY
gender: BOY
gender.ordinal(): 0
================
gender.name(): GIRL
gender.getName(): 女孩
gender.toString(): GIRL
gender: GIRL
gender.ordinal(): 1
注解
1、注解Annotation也被称为元数据Metadata,用于修饰解释 包、类、方法、属性、构造器、局部变量等数据信息
2、和注释一样,注解不影响程序逻辑,但注解可以被编译或运行,相当于嵌入在代码中的补充信息
3、在javaSE中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等
4、在javaEE中,注解占据了更重要的角色,比如用来配置应用程序的任何切面,代替javaEE旧版中所遗留的繁冗代码和XML配置等
使用Annotation时要在其前面增加@符号,并把该Annotation当成一个修饰符使用。
三个基本Annotation
@Override:限定某个方法,是重写父类方法,该注解只能用于方法
@Override表示指定重写父类的方法,如果父类没有该方法,则会报错
如果不写@Override注解,而父类有该方法,仍然构成重写
@Override只能修饰方法,不能修饰其他类、包、属性等
查看@Override注解源码:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
1. @Target(ElementType.METHOD),说明只能修饰方法
2. @Target是修饰注解的注解,称为元注解
3. @interface,表示是一个注解类
@Deprecated:用于表示某个程序元素已过时
可以使用被修饰的元素,但是不推荐使用该元素
可以修饰方法、类、字段、包、参数等等
可以用作版本升级过渡和兼容
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE,
METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
@SuppressWarnings:抑制编译器警告
@SuppressWarnings({“all”})
unchecked:忽略没有检查的警告
rawtypes:忽略没有指定泛型的警告
unused:忽略没有使用某个变量的警告
@SuppressWarnings:可以修饰的程序元素为,查看@Target
@Target({TYPE, FIELD, METHOD, PARAMETER,
CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();//可以存放一个字符串数组,需要忽略的警告
}
元注解:修饰注解的注解
Retention(保留),指定注解的作用范围,SOURCE、CLASS、RUNTIME
只能用于修饰一个Annotation定义,用于指定该Annotation可以保留多长时间,
@Retention包含一个RetentionPolicy类型的成员变量,使用@Rentention时必须为该value成员变量指定值
Retention三种值
@Retention(RetentionPolicy.SOURCE):编译器使用后,直接丢弃这种策略的注释
@Retention(RetentionPolicy.CLASS):编译器将把注释记录在class文件中,当运行java程序时,JVM不会保留注释。这是默认值
@Retention(RetentionPolicy.RUNTIME):编译器将把注释记录在class文件中,当运行java程序时,JVM会保留注释,程序可以通过反射获取该注释
Target
用于修饰Annotation定义,用于指定被修饰的Annotation能用于修饰哪些程序元素,@Target包含一个名为value的成员变量
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}
Documented
用于指定被该元注解修饰的Annotation类将被javadoc工具提取成文档,即在生成文档时,可以看到该注释。
说明:定义为Documented的注释必须设置Retention值为RUNTIME
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
Inherited
被他修饰的Annotation将具有继承性,如果某个类使用了被@Inherited修饰的Annotation,则其子类将自动具有该注释
异常
Java异常的层次结构
异常有哪两大类?Throwable和Exception是什么关系?常见的RuntimeException有哪些?常见的Error有哪些?
Throwable有两个直接的子类: Error、Exception。
Error: 表示一些严重的错误,通常是不应该被应用程序尝试捕获的问题,如OutOfMemoryError,StackOverflowError,InternalError(JVM内部错误)等。
Exception: 表示需要被应用程序捕获的异常条件,它又分为两类:
(1)检查型异常(Checked Exceptions): 这种异常在编译时强制要求处理,比如IOException、SQLException等。Java编译器要求程序必须捕获(try…catch)或声明抛出(方法声明时throws)这种异常。
(2)非检查型异常(Unchecked Exceptions): 这类异常包括运行时异常(RuntimeException)如NullPointerException、IndexOutOfBoundsException等,以及错误(Error)。处理或者不处理都可以(不需try…catch…或在方法声明时throws)。
常见的运行时异常包括:
java.lang.ArithmeticException
java.lang.ArrayStoreExcetpion
java.lang.ClassCastException
java.lang.lllegalArgumentException
java.lang.lllegalThreadStateException
java.lang.NumberFormatException
java.lang.IndexOutOfBoundsException
java.lang.ArraylndexOutOfBoundsException
java.lang.StringIndexOutOfBoundsException
java.lang.NullPointerException
java.lang.SecurityException
java.lang.UnsupprotedoperationException
java.util.ConcurrentModificationException
常见的编译时异常:
① SQLException //操作数据库时,查询表可能发生异常
② IOException //操作文件时,发生的异常
③ FileNotFoundException //当操作一个不存在的文件,发生异常
④ ClassNotFoundException //加载类,而该类不存在时候,异常
⑤ EOFException //操作文件,到文件末尾,发生异常
⑥ IllegalArguementException //参数异常
为什么要对unchecked异常和checked异常进行区分?
编译器将检查你是否为所有的checked异常提供了异常处理机制,比如说我们使用Class.forName()来查找给定的字符串的class对象的时候,如果没有为这个方法提供异常处理,编译是无法通过的。
重写时子父类的异常关系
子类重写父类方法时,对抛出异常的规定:子类重写的方法,所抛出的异常类型要么和父类抛出的异常一致,要么为父类抛出的异常的类型的子类型(针对编译异常,运行异常无所谓);(与重写时的返回值要求差不多)
try/catch/finally的return顺序
(try内都有return)
正常用法:try异常有return, catch/finally无return:try=> catch=> finally=> finally块之外的return
try无异常, finally无return:try=> finally=> try的return
try无异常, finally有return(Idea报警告):try=> finally=> 程序结束(不调用try的return)
(此时catch无所谓)
try有异常, catch有return, finally无return:try=> catch=> finally=> catch的return
try有异常, catch有return, finally有return:try=> catch=> finally=> 程序退出
try有异常, catch有异常, finally无return:try=> catch=> finally=> 程序结束(catch不会再return)
throw和throws的对比
意义 | 位置 | 后面跟的东西 | |
---|---|---|---|
throws | 异常处理的一种方式 | 方法声明处 | 异常类型 |
throw | 手动生成异常对象的关键字 | 方法体中 | 异常对象 |
自定义异常
当程序中出现了某些“错误”,但该错误信息并没有在Throwable子类中描述处理,这个时候可以自己设计异常类,用于描述该错误信息。
步骤
(1)定义类:自定义异常类名(程序员自己写)继承Exception或RuntimeException;
(2)如果继承Exception,属于编译异常;
(3)如果继承RuntimeException,属于运行异常(一般继承RuntimeException,因为有自动调用机制)。
public class CustomException {
public static void main(String[] args) throws AgeException {
int age = 180;
//要求范围在 18 – 120 之间,否则抛出一个自定义异常
if(!(age >= 18 && age <= 120)) {
//这里我们可以通过构造器,设置信息
throw new AgeException("年龄需要在 18~120之间");
}
System.out.println("你的年龄范围正确.");
}
}
//自定义一个异常
//1. 一般情况下,我们自定义异常是继承 RuntimeException
//2. 即把自定义异常做成 运行时异常,好处时,我们可以使用默认的处理机制
//3. 即比较方便
class AgeException extends RuntimeException {
public AgeException(String message) {//构造器
super(message);
}
}
IO
流:数据在数据源(文件)和程序(内存)之间经历的路径
输入流:数据从数据源(文件)到程序(内存)的路径
输出流:数据从程序(内存)到数据源(文件)的路径
自己去了解一下:File(文件类)
FileInputStream、FileOutputStream(节点流:文件字节流)
FileReader、FileWriter(节点流:文件字符流)
BufferInputStream、BufferOutputStream、BufferReader、BufferWriter(包装流)
ObjectInputStream、ObjectOutputStream(包装流:对象流,与序列化/反序列化有关)
流的分类
按操作数据单位不同分为:
字节流(8 bit)二进制文件,(二进制文件:图片,音频,视频)
字符流(按字符)文本文件
按数据流的流向不同分为:输入流,输出流
按流的角色的不同分为:节点流,处理流/包装流
总之,Java的IO流共涉及40多个类,实际上非常规则,都是从4个抽象基类InputStream, OutputStream, Reader, Writer派生的
字节流与字符流的区别
项 | 字节流 | 字符流 |
---|---|---|
操作基本单元 | 字节 | 字符(Unicode码元) |
是否使用缓冲 | 否 | 是。 若频繁对一个资源进行IO操作,会先把需要操作的数据暂时放入内存中,以后直接从内存中读取数据。 这样可以避免多次的IO操作,提高效率。 |
存在位置 | 可存在于文件、内存中。硬盘上的所有文件都是以字节形式存在的。 | 只存在于内存中。 |
使用场景 | 适合操作文本文件之外的文件。 例:图片、音频、视频。 | 适合操作文本文件时使用。 (效率高。因为有缓存) |
Java相关类 | InputStream、OutputStream | Reader、Writer |
节点流和处理流的区别和联系
节点流是底层流/低级流,直接跟数据源相接。
处理流(包装流)包装节点流,既可以消除不同节点流的实现差异,也可以提供更方更的方法来完成输入输出。[源码理解]处理流(也叫包装流)对节点流进行包装,使用了装饰器设计模式,不会直接与数据源相连
处理流的功能主要体现在以下两个方面:
性能的提高:主要以增加缓冲的方式来提高输入输出的效率。
操作的便捷:处理流可能提供了一系列便捷的方法来一次输入输出大批量的数据,使用更加灵活方便
对象流
跟序列化反序列化有关,前面有介绍。
转换流
标准io流
System.in 标准输入InputStream 键盘
System.out 标准输出PrintStream显示器
//在System类中
//public final static InputStream in = null;
InputStream in = System.in;
System.out.println(in.getClass()); //BufferedInputStream
//说明System.in编译类型是InputStream,运行类型是BufferedInputStream
//public Scanner(InputStream source);
Scanner scanner = new Scanner(in); //所以传入参数in没问题
PrintStream out = System.out;
System.out.println(out.getClass()); //PrintStream
//编译与运行类型一致
//表示标准输出--显示屏
转换流
InputStreamReader:Reader的子类,可以将InputStream(字节流)包装成Reader(字符流)
OutputStreamWriter:Writer的子类,实现将OutputStream(字节流)包装成Writer(字符流)
当处理纯文本数据时,如果使用字符流,效率更高,并且可以有效解决中文问题,所以建议将字节流转换成字符流。可以在使用时指定编码格式(比如 utf-8,gbk,gb2312,ISO8859-1 等)
//读取d:\\a.txt到程序
String file = "d:\\a.txt";
//1、文件编码为UTF-8,不报任何错误
BufferedReader br = new BufferedReader(new FileReader(file));
//2、文件编码不为UTF-8,比如ANSI,乱码
// 根本原因就是在于读取文件时没有指定编码方式,
// 而JAVA默认文件是utf-8的格式,那么如果这个文本文件的编码方式是ANSI,
// 也就是系统对应安装的国标码的话,那么用这种普通的方式去读取文件就会出现乱码
// 字节流是可以指定编码方式读取文件的,我们在字节流上指定一个编码方式,
// 就再转成字符流的话,问题就解决了
// 而这就是中间转换流的价值与作用
InputStreamReader isr = null;
OutputStreamWriter_ osw = null;
String s = br.readLine();
System.out.printf("读取到:" + s);
br.close();
public class InputStreamReader_ {
// 将字节流FileInputStream包装成(转换成)
// 字符流InputStreamReader, 对文件进行读取(按照GBK),
// 进而在包装成 BufferedReader
public static void main(String[] args) throws IOException {
String filePath = "d:\\a.txt";
//法1:最外层使用BufferedReader
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(
new FileInputStream(filePath), "GBK"));
String line = bufferedReader.readLine();
System.out.printf(line);
//记得关闭
bufferedReader.close();
//法2:直接使用InputStreamReader,效率不如外层套个BufferedReader高
//还比较麻烦,还是法1好
// InputStreamReader isr = new InputStreamReader(
// new FileInputStream(filePath), "gbk");
// int read;
// while ((read = isr.read()) != -1) {
// System.out.print((char) read);
// }
// isr.close();
}
}
//如果要按照指定的编码格式保存文本文件,那么就用这种方法
public class OutputStreamWriter_ {
public static void main(String[] args) throws IOException {
// 将字节流 FileOutputStream 包装成(转换成)
// 字符流OutputStreamWriter对文件进行写入(按照gbk格式,可以指定其他,比如utf-8)
String filePath = "d:\\c.txt";
String charSet = "gbk"; //会发现保存格式为“ANSI”
//最外层使用BufferedWriter
// BufferedWriter bw = new BufferedWriter(
// new OutputStreamWriter(
// new FileOutputStream(filePath), charSet));
// bw.write("你好,我尊敬的女士。");
// bw.close();
//其实也可以不要BufferedWriter,直接用OutputStreamWriter
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(filePath), charSet);
osw.write("你好呀,同学。");
//不close会写入不了
osw.close();
}
}
打印流
PrintStream是字节流,前面的System.out就是PrintStream类型的
PrintWriter是字符流
Properties类
许多配置信息需要解耦,而不是嵌入代码中,则独立出来一个配置文件。
load: 加载配置文件的键值对到Properties对象
list: 将数据显示到指定设备
getProperty(key): 根据键获取值
setProperty(key,value): 设置键值对到Properties对象
store:将Properties中的键值对存储到配置文件,在idea 中,保存信息到配置文件,如果含有中文,会存储为unicode码
为什么字符流操作文本文件时效率更高?
1、字符流以字符为单位进行操作:这使得它在处理文本文件时更加高效和方便。相比之下,字节流以字节为单位进行操作,处理文本数据时需要更多的转换工作。
2、自动进行字符编码转换:字符流在读写文本数据时会自动进行字符编码转换,如UTF-8、GBK等,这使得它在处理不同编码格式的文本文件时更加灵活和方便。而字节流则需要手动进行编码转换,增加了处理的复杂性和时间成本。
3、缓冲区处理:字符流内部通常会有缓冲区,用于处理Unicode编码,这进一步提高了处理文本数据的效率。字节流虽然也可以使用缓冲区,但其缓冲区是直接以字节为单位进行读写,没有字符流的自动编码转换功能。
Java的IO模型BIO、NIO、AIO
区别:
项 | BIO (Block IO) | NIO (New IO) | AIO(Asynchronous I/O) |
---|---|---|---|
JDK版本 | 所有版本 | JDK1.4及之后 | JDK1.7及之后 |
异步/阻塞 | 同步/阻塞。一个连接一个线程。线程发起IO请求,不管内核是否准备好IO操作,从发起请求起,线程一直阻塞,直到操作完成。数据的读取写入必须阻塞在一个线程内等待其完成。 | 同步/非阻塞。一个请求一个线程。客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。用户进程也需要时不时的询问IO操作是否就绪,这要求用户进程不停的去询问。 | 异步/非阻塞。 一个有效请求一个线程。用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。 |
使用场景 | 已成为解决高并发与大量连接、I/O处理问题的有效方式。比如:Netty、多人聊天室。 | 适用于连接数目多且连接比较长(重操作)的架构。例如:相册服务器。目前 AIO 的应用还不是很广泛。 |
补充知识:
同步请求:A调用B,B的处理是同步的,在处理完之前他不会通知A,只有处理完之后才会明确的通知A。(以煮咖啡为例子,同步煮咖啡,要观察咖啡壶内的咖啡的沸腾程度来判断咖啡有没有煮好)
异步请求:A调用B,B的处理是异步的,B在接到请求后先告诉A我已经接到请求了,然后异步去处理处理完之后通过回调等方式再通知A。(异步煮咖啡,壶插电之后,咖啡煮好之后会通过声音提醒我们可以享用了)
阻塞请求:A调用B,A一直等着B的返回,别的事情什么也不干。(阻塞煮咖啡:
坐在咖啡壶面前等待咖啡煮好:其他什么事情都不做)
非阻塞请求:A调用B,A不用一直等着B的返回,先去忙别的事情了。(非阻塞煮咖啡:开一局apex,等咖啡自己煮好)
以上两组的区别在于:
阻塞、非阻塞说的是调用者,同步、异步说的是被调用者。
同步阻塞:传统咖啡壶,我们一直坐在水壶面前等。咖啡壶同步,我们阻塞。
同步非阻塞:传统咖啡壶,我们让他煮,我们去来一局游戏,时不时去看下煮好了没:咖啡壶同步,我们非阻塞
异步阻塞:自动咖啡壶,在他提醒我们之前,我们一直坐在咖啡壶面前等。这就是异步阻塞:咖啡壶异步了,但是我们阻塞
异步非阻塞:自动咖啡壶,他烧着咖啡:我们去打游戏:咖啡壶煮好之后通知我们:这就是异步非阻塞
BIO:线程发起IO请求后,一直阻塞IO,直到缓冲区数据就绪后,再进入下一步操作。针对网络通信都是一请求一应答的方式,虽然简化了上层的应用开发,但在性能和可靠性方面存在着巨大瓶颈,试想一下如果每个请求都需要新建一个线程来专门处理,那么在高并发的场景下,机器资源很快就会被耗尽。
NIO:线程发起io请求后,立即返回(非阻塞io)。同步指的是必须等待io缓冲区内的数据就绪,而非阴塞指的是,用户线程不原地等待io缓冲区,可以先做一些其他操作,但是要定时轮询检查io缓冲区数据是否就绪。Java中的nio是new io的意思。其实是nio加上io多路复用技术。普通的nio是线程轮询查看一个io缓冲区是否就绪,而java中的nio指的是线程轮询地去查看一堆io缓冲区中哪些就绪,这是一种io多路复用的思想。io多路复用模型中,将检查io数据是否就绪的任务,交给系统级别的select或epoll模型,由系统进行监控,减轻用户线程负担。
AIO:AIO是真正意义上的异步非阻塞IO模型。上述NIO实现中,需要用户线程定时轮询,去检查IO缓冲区数据是否就绪,占用应用程序线程资源,其实轮询相当于还是阻塞的,并非真正解放当前线程,因为它还是需要去查询哪些IO就绪。而真正的理想的异步非阻塞IO应该让内核系统完成用户线程只需要告诉内核,当缓冲区就绪后,通知我或者执行我交给你的回调函数。
AIO可以做到真正的异步的操作,但实现起来比较复杂,支持纯异步IO的操作系统非常少,目前也就windows是IOCP技术实现了,而在Linux上,底层还是是使用的epoll实现的。
网络IO交互
1.网卡收到经过网线传来的网络数据,并将网络数据写到内存中。
2.当网卡把数据写入到内存后,网卡向cpu发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据。
3.将内存中的网络数据写入到对应socket的接收缓冲区中。
4.当接收缓冲区的数据写好之后,应用程序开始进行数据处理,
为什么Netty用NIO?
详细分析:
Linux上,AIO底层实现仍使用Epoll,没有很好的实现AIO,因此性能上没有明显优势,而且被JDK封装了一层不容易优化。
Netty整体架构是基本reactor模型,而AIO是proactor模型,混合在一起会比较混乱。
AIO有个缺点:接收数据需要预先分配缓冲区,而不是NIO那种需要接收时才需要分配缓存,所以对连接数量非常大但流量小的情况,会浪费内存。
Linux上AIO不够成熟,处理回调的结果速度跟不上处理需求,供不应求,造成处理速度有瓶颈。比如:外卖员太少,顾客太多。
另外:NIO是一种基于通道和缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存(区别于JVM的运行时数据区),然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的直接引用进行操作。这样能在一些场景显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
JDK8新特性
接口允许default和static;lambda;stream;时间新API(LocalDateTime等)CompletableFuture;等。
接口允许default和static
前面的“不同JDK版本的接口细节”部分有讲。
Stream API流
流的操作步骤,知道它能进行筛选、去重、排序、分组等操作就行。
JDK8新增了Stream(流操作) 处理集合的数据,可执行查找、过滤和映射数据等操作。
简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。
JDK8 Stream API 流操作包括哪些部分?项目中怎么用的Stream?
Stream操作步骤:创建Stream=> 转换Stream(中间操作)=> 产生结果(终止操作)
注意:这只是一般操作。实际编程时,创建必须有,而中间操作与终止操作是可选的。
这个不用全背过,需要知道的是:流的操作步骤,知道它能进行筛选、去重、排序、分组等操作就行。自学可查看这篇文章。
反射
forName和classLoader的区别
Class.forName()方法也是调用的 ClassLoader 来实现的。
项 | Class.forName | ClassLoader |
---|---|---|
灵活度 | 灵活度低。 例如:加载的类只能是classpath下的 | 灵活度高。 例如:可以自己编写加载类的方法:比如通过读取类文件的二进制数据,这个时候文件可以不存在ClassPath中。 |
是否进行初始化 | 是。 forName方法最后调用 forName0(本地方法),第二个参数设置为了 true,代表对加载的类进行初始化(执行静态代码块、对静态变量赋值) | 否。 |
JDBC为什么用Class.forName?
JDBC 规范中明确要求 Driver(数据库驱动)类必须向 DriverManager 注册。
以 MySQL 的驱动为例,我们看到 Driver 注册到 DriverManager 中的操作写在了静态代码块中,这就是为什么在写 JDBC 时使用 Class.forName() 的原因了。
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
//它有个重载(可以手动选择在加载类时是否要对其类进行初始化。)
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader){}
class Test {
static {
System.out.println("静态代码块");
}
public static final String title = getTitle();
private static String getTitle(){
System.out.println("执行getTitle()");
return "TITLE";
}
}
//实例(forName)
public class Demo {
public static void main(String[] args) {
Class<?> c = null;
try {
c = Class.forName("org.example.a.Test");
}catch (Exception e){
e.printStackTrace();
}
Test test = null;
try {
test = (Test) c.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}
//静态代码块
//执行getTitle()
//实例(ClassLoader)
class Test {
static {
System.out.println("静态代码块");
}
public static final String title = getTitle();
private static String getTitle(){
System.out.println("执行getTitle()");
return "TITLE";
}
}
public class Demo {
public static void main(String[] args) {
Class<?> c = null;
try {
c = ClassLoader.getSystemClassLoader().loadClass("org.example.a.Test");
}catch (Exception e){
e.printStackTrace();
}
Test test = null;
try {
test = (Test) c.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}
//无任何输出
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
/**
* Construct a new driver and register it with DriverManager
*
* @throws SQLException
* if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
反射机制中可以获取private成员的值吗?
public class Demo {
public static void main(String[] args) {
User user = new User();
try {
Class<? extends User> aClass = user.getClass();
Field nameField = aClass.getDeclaredField("name");
nameField.setAccessible(true); //将name属性设置成可被外部访问
nameField.set(user, "Tony"); //设置name属性内容
System.out.println(user.getName());
// 也可以这么写:
// System.out.println(nameField.get(user));
nameField.setAccessible(false); //将name属性设置成不可被外部访问
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Data
class User {
private String name;
private Integer age;
}
反射只能操作本类的private属性,无法操作父类的private属性。如果想操作父类的,需要通过getSuperClass获得父类,然后再操作。
集合专题
新建一篇文章作为,稍后补充链接。
内存泄露的原因及解决方案
如果内存泄露的空间足够大,就会导致内存溢出(OOM)。
内存泄露的原因:
堆内存中一个对象不再使用时,垃圾回收器却无法从内存中删除他们,导致内存泄露。
内存泄露的影响:
- 长时间连续运行时性能严重下降;
- 出现OOM导致应用崩溃;
内存泄露的检测与分析:
通常我们可以借助MAT、LeakCanary等工具来检测应用程序是否存在内存泄漏。
1、MAT是一款强大的内存分析工具,功能繁多而复杂。
2、LeakCanary则是由Square开源的一款轻量级的第三方内存泄漏检测工具,当检测到程序中产生内存泄漏时,它将以最直观的方式告诉我们哪里产生了内存泄漏和导致谁泄漏了而不能被回收。
内存泄露的类型:
主要有以下类型:
ThreadLocal
static字段
未关闭的资源
集合容器
改变哈希值
内部类持有外部类
finalize()方法
常量字符串
ThreadLocal
使用ThreadLocal时,每个线程只要处于存货状态就可保留对其ThreadLocal变量副本的隐式调用,且将保留其自己的副本。使用不当,就会引起内存泄露。
一旦线程不在存在,ThreadLocals就应该被垃圾收集,而现在线程的创建都是使用线程池,线程池有线程重用的功能,因此线程就不会被垃圾回收器回收。所以使用到ThreadLocals来保留线程池中线程的变量副本时,ThreadLocals没有显示的删除时,就会一直保留在内存中,不会被垃圾回收。
解决方法
不再使用ThreadLocal时,调用remove()方法,该方法删除了此变量的当前线程值。
不要使用ThreadLocal.set(null),它只是查找与当前线程关联的Map并将键值对设置为当前线程为null。
static字段
大量使用static字段会潜在的导致内存泄露,在Java中,静态字段通常拥有与整个应用程序相匹配的生命周期。
示例:单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。
解决方法
最大限度的减少静态变量的使用;
单例模式时,依赖于延迟加载对象而不是立即加载方式。
未关闭的资源
对于使用了BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,从而造成内存泄漏。
解决方法
调用它的close()函数将其关闭掉,然后再设置为null
注意
一般使用finally块关闭资源;关闭资源的代码,不应该有异常;
jdk1.7后,可以使用try-with-resource块。
集合容器
我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,如果在不需要该对象时,没有把对象的引用从集合中清理掉,这样这个集合就会越来越大。
如果这个List是临时的,那没问题,List被回收后里边的对象引用也就不会被持有了(对象不可达),对象引用也会被回收。如果这个List不是临时的,那么就会导致内存占用越来越大。
如果这个集合是static的话,那情况就更严重了。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
解决方法
如果是static类型的集合,在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。
改变哈希值
当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了。在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露
解决方法
不要修改这个对象中的那些参与计算哈希值的字段
内部类持有外部类
若一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
在Java中,非静态内部类和匿名类内部类都会潜在持有它们所属的外部类的引用,但是静态内部类却不会。
解决办法
如果内部类不需要访问外部类成员,考虑转换为静态内部类。
finalize()方法
重写finalize()方法时,该类的对象不会立即被垃圾收集器收集,如果finalize()方法的代码有问题,那么会潜在的引发OOM;
解决办法
尽量避免重写finalize();或者保证finalize方法没问题
常量字符串
如果我们读取一个很大的String对象,并调用了intern(),那么它将放到字符串池中,位于PermGen中,只要应用程序运行,该字符串就会保留,这就会占用内存,可能造成OOM。
解决方法
增加PermGen的大小,-XX:MaxPermSize=512m;
升级Java版本,JDK1.7后字符串池转移到了堆中。