Java基础知识之常见面试题整理
声明:本文摘抄自:https://www.cnblogs.com/qing-gee/p/13258782.html
1.请说出Java14版本中更新的重要功能?
1)switch表达式
关于 switch 表达式,这里就简单地举个例子
public class SwitchDemo {
enum PlayerTypes {
TENNIS,
FOOTBALL,
BASKETBALL,
PINGPANG,
UNKNOWN
}
public static void main(String[] args) {
System.out.println(createPlayer(PlayerTypes.BASKETBALL));
}
private static String createPlayer(PlayerTypes playerType) {
return switch (playerType) {
case TENNIS -> "网球运动员费德勒";
case FOOTBALL -> "足球运动员C罗";
case BASKETBALL -> "篮球运动员詹姆斯";
case PINGPANG -> "乒乓球运动员马龙";
case UNKNOWN -> throw new IllegalArgumentException("未知");
};
}
}
除了可以使用 ->
的新式语法,还可以作为 return 结果.
2)instanceof增强表达式,预览功能
旧式的 instanceof 的用法如下所示:
public class OldInstanceOf {
public static void main(String[] args) {
Object str = "Java 14,真香";
if (str instanceof String) {
String s = (String)str;
System.out.println(s.length());
}
}
}
需要先使用 instanceof 在 if 条件中判断 str 的类型是否为 String(第一步),再在 if 语句中将 str 强转为字符串类型(第二步),并且要重新声明一个变量用于强转后的赋值(第三步)。
Java14中的新用法:
public class NewInstanceOf {
public static void main(String[] args) {
Object str = "Java 14,真香";
if (str instanceof String s) {
System.out.println(s.length());
}
}
}
可以直接在 if 条件判断类型的时候添加一个变量,就不需要再强转和声明新的变量了。
3)文本块,第二次预览
在文本块(Text Blocks)出现之前,如果我们需要拼接多行的字符串,就需要很多英文双引号和加号,非常不雅。如果恰好要拼接一些HTML 格式的文本(原生 SQL 也是如此)的话,还要通过空格进行排版,通过换行转义符 \n
进行换行,这些繁琐的工作对于一名开发人员来说,简直就是灾难。
public class OldTextBlock {
public static void main(String[] args) {
String html = "<html>\n" +
" <body>\n" +
" <p>Hello, world</p>\n" +
" </body>\n" +
"</html>\n";
System.out.println(html);
}
}
Java 14 就完全不同了:
public class NewTextBlock {
public static void main(String[] args) {
String html = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
System.out.println(html);
}
}
多余的英文双引号、加号、换行转义符,统统不见了。仅仅是通过前后三个英文双引号就实现了。
4)Records,预览功能
对于 Records 来说,一条 Record 就代表一个不变的状态。
2.请说出Java13版本中更新的重要功能?
1)文本块,预览功能.
2)switch表达式,预览功能.
3)Java Socket 重新实现
4)FileSystems.newFileSystem()方法
5)支持Unicode 12.1
6)可伸缩,低延迟的垃圾收集器改进,用于返回未使用的内存.
3.请说出Java12版本中重要的更新功能?
1)JVM的更新
2)File.mismatch()方法
3)紧凑型数字格式
4)String类新增了一些方法,比如indent()
4.请说出Java11版本中更新的重要功能?
1)可以直接使用Java命令运行Java程序,源代码将会隐式编译和运行.
2)String类新增了一些方法,比如说isBlank(),lines(),strip()等待.
3)Files类新增了两个读写方法,readString()和writeString().
4)可以在Lambda表达式中使用var作为变量类型.
5.请说出Java10版本中更新的重要功能?
1)局部变量类型推断,举个例子:var list = new ArrayList();可以使用var来作为变量类型,Java编译器知道list的类型为字符串的ArrayList;
2)增强java.util.Locale.
3)提供了一种默认的根证书颁发机构(CA)
6.请说出Java9版本中更新的重要功能?
1)模块系统
2)不可变的List,Set,Map的工厂方法
3)接口中可以有私有方法
4)垃圾收集器的改进
7.请说出Java8版本中更新的重要功能?
1)函数式编程和Lambda表达式
Lambda 表达式描述了一个代码块(或者叫匿名方法),可以将其作为参数传递给构造方法或者普通方法以便后续执行。
() -> System.out.println("沉默王二")
从左到右解释一下,()
为 Lambda 表达式的参数列表(本例中没有参数),->
标识这串代码为 Lambda 表达式(也就是说,看到 ->
就知道这是 Lambda),Sy
stem.out.println("沉默王二")
为要执行的代码,即将“沉默王二”打印到标准输出流。
使用Lambda表达式创建一个线程:
原来我们创建一个线程并启动它是这样的:
public class LamadaTest {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("沉默王二");
}
}).start();
}
}
通过 Lambda 表达式只需要下面这样:
public class LamadaTest {
public static void main(String[] args) {
new Thread(() -> System.out.println("沉默王二")).start();
}
}
2)Stream流
Stream 就好像一个高级的迭代器,但只能遍历一次,就好像一江春水向东流;在流的过程中,对流中的元素执行一些操作,比如“过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等。
要想操作流,首先需要有一个数据源,可以是数组或者集合。每次操作都会返回一个新的流对象,方便进行链式操作,但原有的流对象会保持不变。
流的操作可以分为两种类型:
1)中间操作,可以有多个,每次返回一个新的流,可进行链式操作。
2)终端操作,只能有一个,每次执行完,这个流也就用光光了,无法执行下一个操作,因此只能放在最后。
来举个例子。
List<String> list = new ArrayList<>();
list.add("武汉加油");
list.add("中国加油");
list.add("世界加油");
list.add("世界加油");
long count = list.stream().distinct().count();
System.out.println(count);
distinct()
方法是一个中间操作(去重),它会返回一个新的流(没有共同元素)。
Stream<T> distinct();
count()
方法是一个终端操作,返回流中的元素个数。
long count();
中间操作不会立即执行,只有等到终端操作的时候,流才开始真正地遍历,用于映射、过滤等。通俗点说,就是一次遍历执行多个操作,性能就大大提高了。
3)Java Date Time API
4)接口中可以使用默认方法和静态方法
接口是通过 interface 关键字定义的,它可以包含一些常量和方法,来看下面这个示例。
public interface Electronic {
// 常量
String LED = "LED";
// 抽象方法
int getElectricityUse();
// 静态方法
static boolean isEnergyEfficient(String electtronicType) {
return electtronicType.equals(LED);
}
// 默认方法
default void printDescription() {
System.out.println("电子");
}
}
1)接口中定义的变量会在编译的时候自动加上 public static final
修饰符,也就是说 LED 变量其实是一个常量。
2)getElectricityUse()其实是一个抽象方法,没有方法体——这是定义接口的本意。
3)从 Java 8 开始,接口中允许有静态方法,比如说 isEnergyEfficient()
方法。
静态方法无法由(实现了该接口的)类的对象调用,它只能通过接口的名字来调用,比如说 Electronic.isEnergyEfficient("LED")
。
4)接口中允许定义 default
方法也是从 Java 8 开始的,比如说 printDescription
()
,它始终由一个代码块组成,为实现该接口而不覆盖该方法的类提供默认实现,也就是说,无法直接使用一个“;”号来结束默认方法——编译器会报错的。
8.Java面向对象编程中的一些重要概念?
1)抽象
抽象类的介绍:
1)定义抽象类的时候需要用到关键字 abstract
,放在 class
关键字前。
public abstract class AbstractPlayer {
}
2)抽象类不能被实例化,但可以有子类。通过 extends
关键字可以继承抽象类.
public class BasketballPlayer extends AbstractPlayer {
}
3)如果一个类定义了一个或多个抽象方法,那么这个类必须是抽象类。
4)抽象类可以同时声明抽象方法和具体方法,也可以什么方法都没有.
public abstract class AbstractPlayer {
abstract void play();
public void sleep() {
System.out.println("运动员也要休息而不是挑战极限");
}
}
5)抽象类派生的子类必须实现父类中定义的抽象方法。
public class BasketballPlayer extends AbstractPlayer {
@Override
void play() {
System.out.println("我是张伯伦,篮球场上得过 100 分");
}
}
什么时候使用抽象类?抽象类的使用场景?
1)我们希望一些通用的功能被多个子类复用。比如说,AbstractPlayer 抽象类中有一个普通的方法 sleep()
,表明所有运动员都需要休息,那么这个方法就可以被子类复用。
public abstract class AbstractPlayer {
public void sleep() {
System.out.println("运动员也要休息而不是挑战极限");
}
}
BasketballPlayer 继承了 AbstractPlayer 类,也就拥有了 sleep()
方法。
public class BasketballPlayer extends AbstractPlayer {
}
BasketballPlayer 对象可以直接调用 sleep()
方法:
BasketballPlayer basketballPlayer = new BasketballPlayer();
basketballPlayer.sleep();
FootballPlayer 继承了 AbstractPlayer 类,也就拥有了 sleep()
方法。
public class FootballPlayer extends AbstractPlayer {
}
FootballPlayer 对象也可以直接调用 sleep()
方法:
FootballPlayer footballPlayer = new FootballPlayer();
footballPlayer.sleep();
2)我们需要在抽象类中定义好 API,然后在子类中扩展实现。比如说,AbstractPlayer 抽象类中有一个抽象方法 play()
,定义所有运动员都可以从事某项运动,但需要对应子类去扩展实现。
public abstract class AbstractPlayer {
abstract void play();
}
BasketballPlayer 继承了 AbstractPlayer 类,扩展实现了自己的 play()
方法。
public class BasketballPlayer extends AbstractPlayer {
@Override
void play() {
System.out.println("我是张伯伦,我篮球场上得过 100 分,");
}
}
FootballPlayer 继承了 AbstractPlayer 类,扩展实现了自己的 play()
方法。
public class FootballPlayer extends AbstractPlayer {
@Override
void play() {
System.out.println("我是C罗,我能接住任意高度的头球");
}
}
2)封装
3)多态
4)继承
9.什么是JVM?
JVM(Java Virtual Machine)俗称Java虚拟机,之所以称为虚拟机,是因为它实际上并不存在,它提供了一种运行环境,可供Java字节码在上面运行.
JVM提供了以下操作:
1)加载字节码
2)验证字节码
3)执行字节码
4)提供运行时环境
JVM定义了以下内容:
1)存储区
2)类文件格式
3)寄存器组
4)垃圾回收堆
5)致命错误报告等.
JVM的内部结构,它包含了类加载器(ClassLoader),运行时数据区(Runtime Data Areas)和执行引擎(Excution Engine).
1)类加载器:
类加载器是 JVM 的一个子系统,用于加载类文件。每当我们运行一个 Java 程序,它都会由类加载器首先加载。Java 中有三个内置的类加载器:
启动类加载器(Bootstrap Class-Loader),加载 jre/lib
包下面的 jar 文件,比如说常见的rt.jar(包含了 Java 标准库下的所有类文件,比如说 java.lang
包下的类,java.net
包下的类,ja
va.util
包下的类,java.io
包下的类,java.sql
包下的类)。
扩展类加载器(Extension or Ext Class-Loader),加载 jre/lib/ext
包下面的 jar 文件。
应用类加载器(Application or App Clas-Loader),根据程序的类路径(classpath)来加载 Java类。
一般来说,Java 程序员并不需要直接同类加载器进行交互。JVM 默认的行为就已经足够满足大多数情况的需求了。不过,如果遇到了需要和类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就不得不花大量的时间去调试ClassNotFoundException 和 NoClassDefFoundError
等异常。
对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class对象不 equals
)。
通过一段简单的代码了解下。
public class Test {
public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}
每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 类名.class.getClassLoader()
可以获取到此引用;然后通过 loader.getParent()
可以获取类加载器的上层类加载器。
上面这段代码的输出结果如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@4617c264
第一行输出为 Test 的类加载器,即应用类加载器,它是 sun.misc.Launcher$AppClassLoader
类的实例;
第二行输出为扩展类加载器,是 sun.misc.Launcher$ExtClassLoader
类的实例。那启动类加载器呢?
按理说,扩展类加载器的上层类加载器是启动类加载器,但在我这个版本的 JDK 中, 扩展类加载器的 get
Parent()
返回 null
。所以没有输出。
2)运行时数据区
运行时数据区又包含以下内容。
PC寄存器(PC Register),也叫程序计数器(Program Counter Register),是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器,线程私有。
JVM 栈(Java Virtual Machine Stack),与 PC 寄存器一样,JVM 栈也是线程私有的。每一个JVM 线程都有自己的 JVM 栈,这个栈与线程同时创建,它的生命周期与线程相同。它的作用是为虚拟机执行java字节码,或者说来解析java语法是否正确.
本地方法栈(Native Method Stack),JVM 可能会使用到传统的栈来支持 Native 方法(使用Java 语言以外的其它语言[C语言]编写的方法)的执行,这个栈就是本地方法栈。它的作用和JVM差不过,只不过JVM解析的是java字节码,本地方法栈解析其他语言的字节码,比如需要调用c语言的函数,就使用它来解析的.
堆(Heap)在 JVM 中,堆是可供各条线程共享的运行时内存区域,也是供所有类实例和数据对象分配内存的区域。
方法区(Method area),在 JVM 中,被加载类型的信息都保存在方法区中。包括类型信息(TypeInformation)和方法列表(Method Tables)。方法区是所有线程共享的,所以访问方法区信息的方法必须是线程安全的。
运行时常量池(Runtime Constant Pool),运行时常量池是每一个类或接口的常量池在运行时的表现形式,它包括了编译器可知的数值字面量,以及运行期解析后才能获得的方法或字段的引用。简而言之,当一个方法或者变量被引用时,JVM 通过运行时常量区来查找方法或者变量在内存里的实际地址。
3)执行引擎
执行引擎包含了:
解释器:读取字节码流,然后执行指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。
即时(Just-In-Time,JIT)编译器:即时编译器用来弥补解释器的缺点,提高性能。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。
10.JDK和JVM有什么区别?
JDK 是 Java Development Kit 的首字母缩写,是提供给 Java 开发人员的软件环境,包含 JRE 和一组开发工具。可分为以下版本:
标准版(大多数开发人员用的就是这个)
企业版
微型版
JDK 包含了一个私有的 JVM 和一些其他资源,比如说编译器(javac 命令)、解释器(java 命令)等,帮助 Java 程序员完成开发工作。
11.JVM和JRE有什么区别?
Java Runtime Environment(JRE)是 JVM 的实现。JRE 由 JVM 和 Java 二进制文件以及其他类组成,可以执行任何程序。JRE 不包含 Java 编译器,调试器等任何开发工具。
12.Java中main()方法的重要性是什么?
每个程序都需要一个入口,对于 Java 程序来说,入口就是 main 方法。
public static void main(String[] args) {}
public 关键字是另外一个访问修饰符,除了可以声明方法和变量(所有类可见),还可以声明类。main()
方法必须声明为 public。
static 关键字表示该变量或方法是静态变量或静态方法,可以直接通过类访问,不需要实例化对象来访问。
void 关键字用于指定方法没有返回值。
另外,main 关键字为方法的名字,Java 虚拟机在执行程序时会寻找这个标识符;args 为 main()
方法的参数名,它的类型为一个 String 数组,也就是说,在使用 java 命令执行程序的时候,可以给 main()
方法传递字符串数组作为参数。
java HelloWorld 沉默王二 沉默王三//启动类的main方法时传递了两个字符串参数
javac 命令用来编译程序,java 命令用来执行程序,HelloWorld 为这段程序的类名,沉默王二和沉默王三为字符串数组,中间通过空格隔开,然后就可以在 main()
方法中通过 args[0]
和 args[1]
获取传递的参数值了。
public class HelloWorld {
public static void main(String[] args) {
if ("沉默王二".equals(args[0])) {
}
if ("沉默王三".equals(args[1])) {
}
}
}
13.Java的重载(Overload)和重写(Override)有什么区别?
class LaoWang{
public void write() {
System.out.println("老王写了一本《基督山伯爵》");
}
}
public class XiaoWang extends LaoWang {
@Override
public void write() {
System.out.println("小王写了一本《茶花女》");
}
}
重写的两个方法名相同,方法参数的个数也相同;不过一个方法在父类中,另外一个在子类中。就好像父类LaoWang 有一个 write()
方法(无参),方法体是写一本《基督山伯爵》;子类 XiaoWang 重写了父类的 write()
方法(无参),但方法体是写一本《茶花女》。
测试代码:
public class OverridingTest {
public static void main(String[] args) {
LaoWang wang = new XiaoWang();
wang.write();
}
}
测试结果:
小王写了一本《茶花女》
在上面的代码中,们声明了一个类型为 LaoWang 的变量 wang。在编译期间,编译器会检查 LaoWang类是否包含了 write()
方法,发现 LaoWang 类有,于是编译通过。在运行期间,new 了一个XiaoWang 对象,并将其赋值给 wang,此时 Java 虚拟机知道 wang 引用的是 XiaoWang 对象,所以调用的是子类 XiaoWang 中的 write()
方法而不是父类 LaoWang 中的 write()
方法,因此输出结果为“小王写了一本《茶花女》”。
再来看一段重载的代码吧。
class LaoWang{
public void read() {
System.out.println("老王读了一本《Web全栈开发进阶之路》");
}
public void read(String bookname) {
System.out.println("老王读了一本《" + bookname + "》");
}
}
重载的两个方法名相同,但方法参数的个数不同,另外也不涉及到继承,两个方法在同一个类中。就好像类LaoWang 有两个方法,名字都是 read()
,但一个有参数(书名),另外一个没有(只能读写死的一本书)。
测试代码:
public class OverloadingTest {
public static void main(String[] args) {
LaoWang wang = new LaoWang();
wang.read();
wang.read("金瓶");
}
}
这结果就不用猜了。变量 wang 的类型为 LaoWang,wang.read()
调用的是无参的 read()
方法,因此先输出“老王读了一本《Web全栈开发进阶之路》”;wang.read("金瓶")
调用的是有参的 read(booknam
e)
方法,因此后输出“老王读了一本《金瓶》”。在编译期间,编译器就知道这两个 read()
方法是不同的,因为它们的方法签名(=方法名称+方法参数)不同。
简单来总结一下:
1)编译器无法决定调用哪个重写的方法,因为只从变量的类型上是无法做出判断的,要在运行时才能决定;但编译器可以明确地知道该调用哪个重载的方法,因为引用类型是确定的,参数个数决定了该调用哪个方法。
2)多态针对的是重写,而不是重载。
如果在一个类中有多个相同名字的方法,但参数不同,则称为方法重载。
父类中有一个方法,子类中有另外一个和它有相同签名(方法名相同,参数相同、修饰符相同)的方法时,则称为方法重写。子类在重写父类方法的时候可以加一个 @Override
注解。
14.说一下java的访问权限修饰符?
访问权限修饰符对于 Java 来说,非常重要,目前共有四种:public、private、protected 和default(缺省)。
一个类只能使用 public
或者 default
修饰,缺省意味着这个类可以被同一个包下的其他类进行访问;而 public 意味着这个类可以被所有包下的类进行访问。
private 可以用来修饰类的构造方法、字段和方法,只能被当前类进行访问。
protected 也可以用来修饰类的构造方法、字段和方法,但它的权限范围更宽一些,可以被同一个包中的类进行访问,或者当前类的子类。
可以通过下面这张图来对比一下四个权限修饰符之间的差别:
15.final关键字的理解?
1)final 关键字修饰类的时候,表示该类无法被继承。比如,String 类就是 final 的,无法被继承。
2)final 关键字修饰方法的时候,表示子类无法覆盖它,也就是不能重写此方法。
3)final 关键字修饰变量的时候,表示该变量只能被赋值一次,尽管变量的状态可以更改。
3.1)final修饰的字段可以分为两种:一种是static类型的,另一种是没有static类型的
非 static 的 final 字段必须有一个默认值,否则编译器将会提醒没有初始化
static 的 final 字段也叫常量,它的名字应该为大写,可以在声明的时候初始化,也可以通过 static [代码块初始化]()。
3.2)final修饰的参数,final 关键字还可以修饰参数,它意味着参数在方法体内不能被再修改:如果尝试去修改它的话,编译器会提示以下错误:
16.static关键字的理解?
static 关键字可用于变量、方法、代码块和内部类,表示某个特定的成员只属于某个类本身,而不是该类的某个对象。
1)static 关键字可以用来修饰类变量,使其具有全局性,即所有对象将共享同一个变量。它属于一个类,而不是这个类的对象。
2)static 关键字可以用来修饰方法,该方法称为静态方法,只可以访问类的静态变量,并且只能调用类的静态方法。
1.Java 中的静态方法在编译时解析,因为静态方法不能被重写(方法重写发生在运行时阶段,为了多态)。
2.抽象方法不能是静态的。
3.静态方法不能使用 this 和 super 关键字。
4.成员方法可以直接访问其他成员方法和成员变量。
5.成员方法也可以直接访问静态方法和静态变量。
6.静态方法可以访问所有其他静态方法和静态变量。
7.静态方法无法直接访问成员方法和成员变量。
3)静态代码块可以用来初始化静态变量,尽管静态方法也可以在声明的时候直接初始化,但有些时候,我们需要多行代码来完成初始化。
public class StaticBlockDemo {
public static List<String> writes = new ArrayList<>();
static {
writes.add("沉默王二");
writes.add("沉默王三");
writes.add("沉默王四");
System.out.println("第一块");
}
static {
writes.add("沉默王五");
writes.add("沉默王六");
System.out.println("第二块");
}
}
writes 是一个静态的 ArrayList,所以不太可能在声明的时候完成初始化,因此需要在静态代码块中完成初始化。
1.一个类可以有多个静态代码块。
2.静态代码块的解析和执行顺序和它在类中的位置保持一致。
4)静态内部类
Java 允许我们在一个类中声明一个内部类,它提供了一种令人信服的方式,允许我们只在一个地方使用一些变量,使代码更具有条理性和可读性。
常见的内部类有四种,成员内部类、局部内部类、匿名内部类和静态内部类.
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
public static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
这就是创建单例的一种方式,第一次加载Singleton 类时并不会初始化 instance,只有第一次调用 getInstance()
方法时Java 虚拟机才开始加载 SingletonHolder 并初始化 instance,这样不仅能确保线程安全也能保证 Singleton 类的唯一性。
1.静态内部类不能访问外部类的所有成员变量。
2.静态内部类可以访问外部类的所有静态变量,包括私有变量。
3.外部类不能声明为 static。
17.finally和finalize的区别?
1)finally 通常与 try-catch 块一起使用,即使 try-catch 块引发了异常,finally 块中的代码也会被执行,用于释放 try 块中创建的资源。
2)finalize()
是 Object 类的一个特殊方法,当对象正在被垃圾回收时,垃圾收集器将会调用该方法。可以重写该方法用于释放系统资源。
18.可以将一个类声明为static的吗?
不能将一个外部类声明为 static 的,但可以将一个内部类声明为 static 的——称为静态内部类。
19.什么是try-with-resources?
try-with-resources 是 Java 7 时引入的一个自动资源管理语句,在此之前,我们必须通过 try-catch-finally 的方式手动关闭资源,当我们忘记关闭资源的时候,就容易导致内存泄漏。
以前我们完成读取文件的功能时,经常会使用try-catch-finally,在finally语句中关闭资源:
public class TrycatchfinallyDecoder {
public static void main(String[] args) {
BufferedReader br = null;
try {
String path = TrycatchfinallyDecoder.class.getResource("/牛逼.txt").getFile();//确定文件在resource文件下
String decodePath = URLDecoder.decode(path,"utf-8");//解决读取中文名的文件名不乱码问题
br = new BufferedReader(new FileReader(decodePath));
String str = null;
while ((str =br.readLine()) != null) {
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
} finally {//关闭资源
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
try–catch-finally 至始至终存在一个严重的隐患:try 中的 br.readLine()
有可能会抛出 IOException
,finally 中的 br.close()
也有可能会抛出 IOExceptio
n
。假如两处都不幸地抛出了 IOException,那程序的调试任务就变得复杂了起来,而且还有可能br.close()的异常覆盖br.readLine()的异常,到底是哪一处出了错误,就需要花一番功夫,这是我们不愿意看到的结果。
但自从有了 try-with-resources,这些问题就迎刃而解了,只要需要释放的资源(比如 BufferedReader)实现了 AutoCloseable 接口。有了解决方案之后,我们来对之前的 finally 代码块进行瘦身。
try (BufferedReader br = new BufferedReader(new FileReader(decodePath));) {//把要释放的资源写在 try 后的 ()
中
String str = null;
while ((str =br.readLine()) != null) {
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
}//finally 代码块消失了
如果有多个资源(BufferedReader 和 PrintWriter)需要释放的话,可以直接在()
中添加。
try (BufferedReader br = new BufferedReader(new FileReader(decodePath));
PrintWriter writer = new PrintWriter(new File(writePath))) {//有多个资源需要释放的时候
String str = null;
while ((str =br.readLine()) != null) {
writer.print(str);
}
} catch (IOException e) {
e.printStackTrace();
}
如果你想释放自定义资源的话,只要让它实现 AutoCloseable 接口,并提供 close
()
方法即可。
class MyResource implements AutoCloseable {
@Override
public void close() throws Exception {
System.out.println("关闭自定义资源");
}
}
public class TrywithresourcesCustom {
public static void main(String[] args) {
try (MyResource resource = new MyResource();) {
} catch (Exception e) {
e.printStackTrace();
}
}
}
代码运行后输出的结果如下所示:
关闭自定义资源
我们在 try ()
中只是 new 了一个 MyResource 的对象,其他什么也没干,但偏偏 close()
方法中的输出语句执行了。想要知道为什么吗?来看看反编译后的字节码吧。
class MyResource implements AutoCloseable {
MyResource() {
}
public void close() throws Exception {
System.out.println("关闭自定义资源");
}
}
public class TrywithresourcesCustom {
public TrywithresourcesCustom() {
}
public static void main(String[] args) {
try {
MyResource resource = new MyResource();
resource.close();//自己主动调用了close方法
} catch (Exception var2) {
var2.printStackTrace();
}
}
}
20.什么是multi-catch?
Java 7 改进的另外一个地方就是 multi-catch,可以在单个 catch 中捕获多个异常,当一个 try 块抛出多多个类似的异常时,这种写法更短,更清晰。
catch(IOException | SQLException ex){
logger.error(ex);
throw new MyException(ex.getMessage());
}
当有多个异常的时候,可以使用管道表示符“|”隔开。
21.什么是static块?
static 块是由 Java ClassLoader 将类加载到内存中时执行的代码块。通常用于初始化类的静态变量或者创建静态资源。
22.说一下你对java中接口的理解?
接口是 Java 编程语言中的一个核心概念,不仅在 JDK 源码中使用很多,还在 Java 设计模式、框架和工具中使用很多。接口提供了一种在 Java 中实现抽象的方法,用于定义子类的行为约定。
接口和抽象类的区别?
1)语法层面上:
接口中不能有 public 和 protected 修饰的方法,抽象类中可以有。
接口中的变量只能是隐式的常量,抽象类中可以有任意类型的变量。
一个类只能继承一个抽象类,但却可以实现多个接口。
2)设计层面上:
抽象类是对类的一种抽象,继承抽象类的类和抽象类本身是一种 is-a
的关系。
接口是对类的某种行为的一种抽象,接口和类之间并没有很强的关联关系,所有的类都可以实现 Serializable
接口,从而具有序列化的功能。
23.说一下String类的理解,以及对不可变类(t_claim_green_flag类)的了解?
什么是不可变类:
一个类的对象在通过构造方法创建后如果状态不会再被改变,那么它就是一个不可变(immutable)类。它的所有成员变量的赋值仅在构造方法中完成,不会提供任何setter 方法供外部类去修改。
String类为什么要设计成不可变的?
1)常量池的需要
字符串常量池是 Java 堆内存中一个特殊的存储区域,当创建一个 String 对象时,假如此字符串在常量池中不存在,那么就创建一个;假如已经存,就不会再创建了,而是直接引用已经存在的对象。这样做能够减少 JVM 的内存开销,提高效率。
2)hashCode的需要
因为字符串是不可变的,所以在它创建的时候,其 hashCode 就被缓存了,因此非常适合作为哈希值(比如说作为 HashMap 的键),多次调用只返回同一个值,来提高效率。
3)线程安全
就像之前说的那样,如果对象的状态是可变的,那么在多线程环境下,就很容易造成不可预期的结果。而 String 是不可变的,就可以在多个线程之间共享,不需要同步处理。
因此,当我们调用 String 类的任何方法(比如说 trim()
、substring()
、toLowerC
ase()
)时,总会返回一个新的对象,而不影响之前的值。
除了 String 类,包装器类 Integer、Long 等也是不可变类。
自定义一个不可变类:
一个不可变类,必须要满足以下4 个条件:
1)确保类是 final 的,不允许被其他类继承。
2)确保所有的成员变量(字段)是 final 的,这样的话,它们就只能在构造方法中初始化值,并且不会在随后被修改。
3)不要提供任何 setter 方法。
4)如果要修改类的状态,必须返回一个新的对象。
按照以上条件,我们来自定义一个简单的不可变类 Writer。
public final class Writer {
private final String name;
private final int age;
public Writer(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
}
Writer 类是 final 的,name 和 age 也是 final 的,没有 setter 方法。
定义一个可变类:Book 类
public class Book {
private String name;
private int price;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
2 个字段,分别是 name 和 price,以及 getter 和 setter,重写后的 toString()
方法。然后,在 Writer 类中追加一个可变对象字段 book。
public final class Writer {
private final String name;
private final int age;
private final Book book;
public Writer(String name, int age, Book book) {
this.name = name;
this.age = age;
this.book = book;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public Book getBook() {
return book;
}
}
并在构造方法中追加了 Book 参数,以及 Book 的 getter 方法。
完成以上工作后,我们来新建一个测试类,看看 Writer 类的状态是否真的不可变。
public class WriterDemo {
public static void main(String[] args) {
Book book = new Book();
book.setName("Web全栈开发进阶之路");
book.setPrice(79);
Writer writer = new Writer("沉默王二",18, book);
System.out.println("定价:" + writer.getBook());
writer.getBook().setPrice(59);
System.out.println("促销价:" + writer.getBook());
}
}
程序输出的结果如下所示:
定价:Book{name='Web全栈开发进阶之路', price=79}
促销价:Book{name='Web全栈开发进阶之路', price=59}
糟糕,Writer 类的不可变性被破坏了,价格发生了变化。为了解决这个问题,我们需要为不可变类的定义规则追加一条内容:
如果一个不可变类中包含了可变类的对象,那么就需要确保返回的是可变对象的副本。也就是说,Writer 类中的 getBook()
方法应该修改为:
public Book getBook() {
Book clone = new Book();
clone.setPrice(this.book.getPrice());
clone.setName(this.book.getName());
return clone;
}
这样的话,构造方法初始化后的 Book 对象就不会再被修改了。此时,运行WriterDemo,就会发现价格不再发生变化了。
定价:Book{name='Web全栈开发进阶之路', price=79}
促销价:Book{name='Web全栈开发进阶之路', price=79}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· Blazor Hybrid适配到HarmonyOS系统
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 分享4款.NET开源、免费、实用的商城系统
· 解决跨域问题的这6种方案,真香!
· 一套基于 Material Design 规范实现的 Blazor 和 Razor 通用组件库