Java面试题(Java基础)
- 一、Java基础
- 1. 基础
- 2. String
- 3.static
- 4.异常
- 5、IO
- 7、重载和重写的区别
- 8、接口和抽象类的区别
- 9、List和Set的区别
- 10、ArrayList和LinkedList区别
- 11、HashMap和HashTable有什么区别?其底层实现是什么?
- 12、谈谈ConcurrentHashMap的扩容机制
- 13、Jdk.到Jdk.HashMap发生了什么变化(底层)?
- 14、说一下HashMap的Put方法
- 15、泛型中extends和super的区别
- 16、深拷贝和浅拷贝
- 17、HashMap的扩容机制原理
- 18、CopyOnWriteArrayList的底层原理是怎样的什么是字节码?采用字节码的好处是什么?
- 19、Java中的异常体系是怎样的
- 20、Java中有哪些类加载器,说说类加载器双亲委派模型
- 21、GC如何判断对象可以被回收
- 22、JVM中哪些是线程共享区
- 23、你们项目如何排查JVM问题
- 24、一个对象从加载到JVM,再到被GC清除,都经历了什么过程?
- 25、怎么确定一个对象到底是不是垃圾?
- 26、什么是STW?
- 27、JVM有哪些垃圾回收器?垃圾回收分为哪些阶段
- 28、什么是三色标记?
- 29、JVM参数有哪些?
- 二、Java并发(20)
- 1、线程的生命周期?线程有几种状态
- 2、sleep()、wait()、join()、yielo()红超间的区别
- 3、对线程安全的理解
- 4、Thread和Runable的区别
- 5、对守护线程的理解
- 6、ThreadLocal的底层原理
- 7、并发、并行、串行之间的区别
- 8、并发的三大特性
- 9、Java死锁如何避免?
- 10、如何理解volatile关键字
- 11、为什么用线程池?解释下线程池参数?
- 12、线程池的底层工作原理
- 13、线程池中阻塞队列的作用?为什么是先添加列队?
- 14、线程池中线程复用原理
- 15、ReentrantLock中的公平锁和非公平锁的底层实现
- 16、ReentrantLock中tryLock0和lock0方法的区别
- 17、CountDownLatch和Semaphore的区别和底层原理
- 18、Sychronized的偏向锁、轻量级锁、重量级锁
- 19、Sychronized和ReentrantLock的区别谈谈你对AQS的理解,AQS如何实现可重入锁?
一、Java基础
1. 基础
1、面向对象
2、JDK、JRE、JVM之间的区别
3、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的值是否相同。
4、hashCode()与equals()之间的关系
5、final关键字的作用是什么?
2. String
1、String、StringBuffer、StringBuilder的区别
2、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 com.iweb;
public class Demo {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);
String s3 = "abc" + s1 + "def";
String s4 = "abcabcdef";
System.out.println(s3 == s4);
String s5 = s3.intern();
System.out.println(s4 == s5);
}
}
运行结果
true
false
true
示例3:new String(“xx”) + new String(“xx”)
题目:以下语句创建多少个对象?
String s = new String(“abc”) + new String(“abc”);
答案:创建4个String对象
分析: JVM先在String池中创建1个String对象存储”abc”。之后,遇到new关键字,再在内存堆上创建1个String对象存储”abc”,其char value[]则指向常量池中的char value[];String池中已有”abc”的对象,所以第二个new语句不在String池中创建对象,只在内存堆上创建1个String对象;两个字符串相加会在堆上创建1个String对象”abcabc”。(因为没有显式使用双引号指定,也没有调用intern,所以字符串池里边目前没有“abcabc”对象)
示例4:字面量+new String(“xx”)
题目:以下语句创建多少个对象?
String s = “abc” + new String(“def”);
答案:创建4个String对象
分析:
JVM先在String池中创建2个String对象存储”abc”和”def”。之后,遇到new关键字,再在内存堆上创建1个String对象存储”def”,其char value[]则指向常量池中的char value[];
两个字符串相加会在堆上创建1个String对象”abcdef”。(因为没有显式使用双引号指定,也没有调用intern,所以字符串池里边目前没有“abcdef”对象)
3、intern方法的作用及原理
1、常量池简介
在 JAVA 语言中有8种基本类型和一种比较特殊的类型String。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池(在方法区)的概念。常量池就类似一个JAVA系统级别提供的缓存。8种基本类型的常量池都是系统协调的,String类型的常量池比较特殊。String的常量池的主要使用方法有两种:
- 直接使用双引号声明出来的String对象会直接存储在常量池中。
- 如果不是用双引号声明的String对象,可以使用String提供的intern方法将其放到常量池。
2、intern方法简介(JDK7之后)
原型:public native String intern();
说明:从字符串常量池中查询当前字符串是否存在(通过equals判断)。
如果存在,返回常量池中的字符串引用。
如果不存在,把这个String对象引用存到常量池,然后返回这个String对象的引用。
返回值:都是返回String变量对应的常量池中字符串的引用。
以上内容可以从intern的注释中得到:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// 其他代码
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
}
4、String不可变的含义、原因、好处
String不可变的含义
String不可变的含义是:将一个已有字符串"123"重新赋值成"456",不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。也就是说:不可变的含义是内部数据不可变,而不是说引用不可变。
示例代码:
package com.example.a;
public class Demo {
public static void main(String[] args) {
String str= "123";
str = "456";
System.out.println(str);
}
}
执行第一行代码时,在堆上新建一个对象实例 123。str 是指向该实例的引用,是实例在堆上的内存地址而已。
执行第二行代码时,只是改变了 str 这个引用的地址,指向了另一个实例 456。
所以,正如前面所说过的,不可变类是其实例不能被修改的类。 给 str 重新赋值仅仅只是改变了它的引用而已,并不会真正去改变它本来的内存地址上的值。
String为什么不可变
String的内部数据是一个char数组,是对字符串数组的封装,并且是被final修饰的,创建后不可改变。
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来说,好处是:
- 便于实现字符串池(String pool)
在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。
如果字符串是可变的,某一个字符串变量改变了其值,那么其指向的变量的值也会改变,String pool将不能够实现!
例如String a = "Hello World!";
这个代码,就只会在字符串池创建一个字符串“Hello World!”: - 使多线程安全
看下面这个场景,一个函数appendStr()在不可变的String参数后面加上一段“bbb”后返回。appendSb()负责在可变的StringBuilder后面加"bbb"。
public class test {
// 不可变的String
public static String appendStr(String s) {
s += "bbb";
return s;
}
// 可变的StringBuilder
public static StringBuilder appendSb(StringBuilder sb) {
return sb.append("bbb");
}
public static void main(String[] args) {
String s = new String("aaa");
String ns = test.appendStr(s);
System.out.println("String aaa>>>" + s.toString());
// StringBuilder做参数
StringBuilder sb = new StringBuilder("aaa");
StringBuilder nsb = test.appendSb(sb);
System.out.println("StringBuilder aaa >>>" + sb.toString());
}
}
如果程序员不小心像上面例子里,直接在传进来的参数上加上“bbb”。因为Java对象参数传的是引用,所有可变的StringBuffer参数就被改变了。可以看到变量sb在Test.appendSb(sb)操作之,就变成了"aaabbb"。
有的时候这可能不是程序员的本意。所以String不可变的安全性就体现在这里。
在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。
-
避免安全问题
在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。
因为String是不可变的,所以它的值是不可改变的。但由于String不可变,也就没有任何方式能修改字符串的值,每一次修改都将产生新的字符串,如果使用char[]来保存密码,仍然能够将其中所有的元素设置为空和清零,也不会被放入字符串缓存池中,用字符串数组来保存密码会更好。 -
加快字符串处理速度
由于String是不可变的,保证了hashCode的唯一性,于是在创建对象时其hashCode就可以放心的缓存了,不需要重新计算。这也就是一般将String作为Map的Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。
在String类的定义中有如下代码:private int hash; // 用来缓存HashCode
String的字符数据可变
String类使用char value[]来存字符数据,它的类型为:private final char value[];
看上去它是不可更改的,因为是final类型。注意:final只是表示不能指向其他地址,它里边的内容是可以更改的。
结论:String是可以更改的,使用反射,value.setAccessible(true),然后修改它即可。
示例
package org.example.a;
import java.lang.reflect.Field;
public class Demo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s = "1234";
System.out.println("改变前:s=" + s);
Field f = s.getClass().getDeclaredField("value");
f.setAccessible(true);
f.set(s, new char[]{'a', 'b', 'c'});
System.out.println("改变后:s=" + s);
}
}
执行结果
改变前:s=1234
改变后:s=abc
3.static
1、static的五种用法/使用位置/实例
用法1:修饰成员属性
给属性加了static关键字之后,对象就不再拥有该属性了,该属性会由类去管理,即多个对象只对应一个属性。
使用场景:一般用于定义一些常量。
示例:
package com.example.a;
class Test{
public static String NAME = "Tony";
}
public class Demo {
public static void main(String[] args) {
System.out.println(Test.NAME);
}
}
执行结果
Tony
用法2:修饰成员方法
方法本来就是存放在类的定义当中的。static修饰成员方法的作用是可以使用"类名.方法名"的方式操作方法,避免了先要new出对象的繁琐和资源消耗。
示例:
package com.example.a;
class Test{
public static void sayHello(String name) {
System.out.println("Hello," + name);
}
}
public class Demo {
public static void main(String[] args) {
Test.sayHello("Tony");
}
}
结果
Hello,Tony
用法3:修饰代码块
static { }就是静态块,当类加载器载入类的时候,这一部分代码被执行,常用于对静态变量进行初始化工作。当其他代码用到这个类,类加载器才会将它载入。
代码只能执行一次,不能再调用。在静态块中,可以访问静态变量,调用静态方法。
如果去掉static,{ }中的代码则会在创建类对象的时候才执行,(相当于把这部分代码复制到各个构造函数中),这样可以实现块中的内容在多个构造函数中的复用。
package com.example.a;
class Test{
static {
System.out.println("This is static Block");
}
{
System.out.println("This is Block");
}
public Test(){
System.out.println("This is Test()");
}
public Test(String string){
System.out.println("This is Test(String string)");
}
}
public class Demo {
public static void main(String[] args) {
System.out.println("------------------------");
Test test1 = new Test();
System.out.println("------------------------");
Test test2 = new Test("Hello");
}
}
执行结果
------------------------
This is static Block
This is Block
This is Test()
------------------------
This is Block
This is Test(String string)
用法4:修饰内部类
static不能修饰普通类,但可以修饰内部类。原因如下:
static修饰的东西被我们成为类成员,它会随着类的加载而加载,比如:静态代码块,静态成员,静态方法(这里只是加载,并没有调用)等等。若把一个Class文件中的外部类设为static,那目的何在呢?难道让这个类随着应用的启动而加载吗?如果我在这次使用过程中根本没有使用过这个类,那么是不是就会浪费内存。这样来说设计不合理,总而言之,设计不合理的地方,Java是不会让它存在的。
为什么内部类可以使用static修饰呢,因为内部类算是类的成员了,如果我们没有使用静态来修饰,那么我们在创建内部类的时候就需要先有一个外部类的对象,如果我们一直在使用内部类,那么内存中就会一直存在外部类的引用,而我们有时候只需要使用内部类,不需要外部类,那么还是会浪费内存,甚至会造成内存溢出。使用static修饰内部类之后,内部类在创建对象时就不需要有外部类对象的引用了。
实例:
单例模式的静态内部类写法
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
public class Demo{
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
}
}
用法5:静态导包
导入时,使用static关键字,而且在引入类的最后还加上了“.*”,它的作用就是将PrintHelper类中的所有类方法直接导入。不同于非static导入,采用static导入包后,在不与当前类的方法名冲突的情况下,无需使用“类名.方法名”的方法去调用类方法了,直接可以采用"方法名"去调用类方法,就好像是该类自己的方法一样使用即可。
/* PrintHelper.java文件 */
package com.dotgua.study;
public class PrintHelper {
public static void print(Object o){
System.out.println(o);
}
}
import static com.dotgua.study.PrintHelper.*;
public class Demo{
public static void main( String[] args )
{
print("Hello World!");
}
}
static加载顺序
首先思考,下边程序是否能编译通过?若可以编译通过,那么执行结果是什么?
package com.example.a;
class User{
private static String name;
public void setName(String name) {
this.name = name;
}
public static String getName() {
return User.name;
}
}
public class Demo {
public static void main(String[] args) {
new User().setName("Tony");
System.out.println(User.getName());
}
}
结果
Tony
可见,实例对象可以访问访问类变量。
如果将setName改为static类型,则无法编译通过。static里边不能有this,这与类加载流程有关。
2、JVM原理–类加载过程(有实例)(为什么静态方法不能调用非静态方法和变量?)
与类加载顺序有关,加载静态方法时,非静态的未初始化。
类加载过程
流程概述:
加载=> 链接(验证+准备+解析)=> 初始化=> 使用=> 卸载
- 加载(将硬盘上的Java二进制文件(class文件)转为内存中的Class对象)
通过一个类的全限定名获取定义此类的二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存(不一定在堆中,HotSpot是在方法区)中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。 - 链接(给静态变量赋初始值,符号引用替换成直接引用)
验证:检查载入的class文件数据的正确性
准备:给类变量(静态变量)分配内存(方法区)并设置为零值(0、false、null等)。
例外:static final类型的String或基本类型,直接赋值为最终值,如:static final int a = 12; 在准备阶段就将a赋值为12。
解析(可选):将常量池内的符号引用替换成直接引用。
符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用 ,那引用的目标必定已经在内存中存在。 - 初始化(初始化类变量(静态变量)、执行静态语句块)
执行类变量(静态变量)的赋值动作和静态语句块(按定义的顺序从上往下执行)。优先级:静态、父类、子类
注意:初始化是操作类变量(也就是静态变量),不是对象的变量。
使用(以new一个对象为例)
若是第一次创建 Dog 对象(对象所属的类没有加载到内存中)则先执行上面的初始化操作。
在堆上为 Dog 对象(包括实例变量)分配空间,所有属性都设成默认值(数字为 0,字符为 null,布尔为 false,引用被设成 null)
初始化实例:给实例变量赋值、执行初始化语句块
执行构造函数检查是否有父类,如果有父类会先调用父类的构造函数
执行本类的构造函数。
4.异常
1、异常的类型及原理
异常的层次结构
Throwable有两个直接的子类: Error、Exception。
- Error
– JVM内部的严重问题,比如资源不足等,无法恢复。
– 处理方式: 程序员不用处理 - Exception
– JVM通过处理还可回到正常执行流程,即:可恢复。
– 分RuntimeException和其他Exception,或者说分为非受检异常(unchecked
exception)和受检异常(checked exception)。 使用建议:将checked
exceptions用于可恢复的情况,将unchecked exception用于编程的错误。 Use checked
exceptions for recoverable conditions and runtime exceptions for
programming errors (Item 58 in 2nd edition)
– RuntimeException(unchecked exception)
处理或者不处理都可以(不需try…catch…或在方法声明时throws)
– 其他Exception(checked exception)
Java编译器要求程序必须捕获(try…catch)或声明抛出(方法声明时throws)这种异常。
为什么要对unchecked异常和checked异常进行区分?
编译器将检查你是否为所有的checked异常提供了异常处理机制,比如说我们使用Class.forName()来查找给定的字符串的class对象的时候,如果没有为这个方法提供异常处理,编译是无法通过的。
捕获异常再抛出
注意:下边的异常中的打印要用System.err而不是System.out,不然会影响打印的顺序。
实例1:包裹原异常再抛出
package com.example.a;
public class Demo {
public static void main(String[] args) throws Exception {
try {
int i = 1 / 0;
} catch (Exception e) {
System.err.println("自己捕获到异常:" + e.getMessage());
e.printStackTrace();
System.err.println("自己打印异常结束");
throw new Exception("自己抛出的异常", e);
}finally {
System.out.println("This is finally");
}
}
}
执行结果
自己捕获到异常:/ by zero
java.lang.ArithmeticException: / by zero
at com.example.a.Demo.main(Demo.java:6)
自己打印异常结束
Exception in thread "main" java.lang.Exception: 自己抛出的异常
at com.example.a.Demo.main(Demo.java:11)
Caused by: java.lang.ArithmeticException: / by zero
at com.example.a.Demo.main(Demo.java:6)
This is finally
实例2:不包裹原异常再抛出
package com.example.a;
public class Demo {
public static void main(String[] args) throws Exception {
try {
int i = 1 / 0;
} catch (Exception e) {
System.err.println("自己捕获到异常:" + e.getMessage());
e.printStackTrace();
System.err.println("自己打印异常结束");
throw new Exception("自己抛出的异常");
}finally {
System.out.println("This is finally");
}
}
}
执行结果
自己捕获到异常:/ by zero
java.lang.ArithmeticException: / by zero
at com.example.a.Demo.main(Demo.java:6)
自己打印异常结束
Exception in thread "main" java.lang.Exception: 自己抛出的异常
at com.example.a.Demo.main(Demo.java:11)
This is finally
2、捕获异常(Exception)时,try/catch/finally的return顺序
- 如果catch或者finally中有return,则catch和finally代码块之后的部分根本不会执行到。
- 如果catch和finally中都有return,后边的(也就是finally的)return会作为返回值。
- try,catch执行到了return之前都会执行finally
- 不要在try里return。
正常用法(try异常, catch/finally无return)
package org.example.a;
public class Demo {
public static void main(String[] args) {
Object handler = handler();
System.out.println(handler.toString());
}
public static Object handler() {
try {
System.out.println("try:内(前)");
System.out.println("try:内(异常)" + 5 / 0);
System.out.println("try:内(后)");
return "try:返回";
} catch (Exception e) {
System.out.println("catch:内(前)");
// System.out.println("catch:内(异常)" + 5 / 0);
// System.out.println("catch:内(后)");
// return "catch:返回";
} finally {
System.out.println("finally:内");
// return "finally:返回";
}
System.out.println("最后");
return "最后(返回)";
}
}
执行结果(try=> catch=> finally=> finally块之外)
try:内(前)
catch:内(前)
finally:内
最后
最后(返回)
try无异常, catch有return, finally无return
package org.example.a;
public class Demo {
public static void main(String[] args) {
Object handler = handler();
System.out.println(handler.toString());
}
public static Object handler() {
try {
System.out.println("try:内(前)");
// System.out.println("try:内(异常)" + 5 / 0);
// System.out.println("try:内(后)");
return "try:返回";
} catch (Exception e) {
System.out.println("catch:内(前)");
System.out.println("catch:内(异常)" + 5 / 0);
System.out.println("catch:内(后)");
return "catch:返回";
} finally {
System.out.println("finally:内");
// return "finally:返回";
}
}
}
执行结果(try=> finally=> try的return)
try:内(前)
finally:内
try:返回
try无异常, finally有return(Idea报警告)
package org.example.a;
public class Demo {
public static void main(String[] args) {
Object handler = handler();
System.out.println(handler.toString());
}
public static Object handler() {
try {
System.out.println("try:内(前)");
// System.out.println("try:内(异常)" + 5 / 0);
// System.out.println("try:内(后)");
return "try:返回";
} catch (Exception e) {
System.out.println("catch:内(前)");
System.out.println("catch:内(异常)" + 5 / 0);
System.out.println("catch:内(后)");
return "catch:返回";
} finally {
System.out.println("finally:内");
return "finally:返回";
}
}
}
执行结果(try=> finally=> 程序结束(不调用try的return))
try:内(前)
finally:内
finally:返回
try有异常, catch有return, finally无return
package org.example.a;
public class Demo {
public static void main(String[] args) {
Object handler = handler();
System.out.println(handler.toString());
}
public static Object handler() {
try {
System.out.println("try:内(前)");
System.out.println("try:内(异常)" + 5 / 0);
System.out.println("try:内(后)");
return "try:返回";
} catch (Exception e) {
System.out.println("catch:内(前)");
// System.out.println("catch:内(异常)" + 5 / 0);
// System.out.println("catch:内(后)");
return "catch:返回";
} finally {
System.out.println("finally:内");
// return "finally:返回";
}
}
}
执行结果(try=> catch=> finally=> catch的return)
try:内(前)
catch:内(前)
finally:内
catch:返回
try有异常, catch有return, finally有return
package org.example.a;
public class Demo {
public static void main(String[] args) {
Object handler = handler();
System.out.println(handler.toString());
}
public static Object handler() {
try {
System.out.println("try:内(前)");
System.out.println("try:内(异常)" + 5 / 0);
System.out.println("try:内(后)");
return "try:返回";
} catch (Exception e) {
System.out.println("catch:内(前)");
// System.out.println("catch:内(异常)" + 5 / 0);
// System.out.println("catch:内(后)");
return "catch:返回";
} finally {
System.out.println("finally:内");
return "finally:返回";
}
}
}
执行结果(try=> catch=> finally=> 程序退出)
try:内(前)
catch:内(前)
finally:内
finally:返回
try有异常, catch有异常, finally无return
package org.example.a;
public class Demo {
public static void main(String[] args) {
Object handler = handler();
System.out.println(handler.toString());
}
public static Object handler() {
try {
System.out.println("try:内(前)");
System.out.println("try:内(异常)" + 5 / 0);
System.out.println("try:内(后)");
return "try:返回";
} catch (Exception e) {
System.out.println("catch:内(前)");
System.out.println("catch:内(异常)" + 5 / 0);
System.out.println("catch:内(后)");
return "catch:返回";
} finally {
System.out.println("finally:内");
// return "finally:返回";
}
}
}
执行结果(try=> catch=> finally=> 程序结束(catch不会再return))
Exception in thread "main" java.lang.ArithmeticException: / by zero
at org.example.a.Demo.handler(Demo.java:18)
at org.example.a.Demo.main(Demo.java:5)
try:内(前)
catch:内(前)
finally:内
5、IO
1、字节流与字符流的相同点与区别
相同点:
字符流和字节流都有缓冲的实现类,提高了读写的效率:
字符流:BufferedReader、BufferedWriter
字节流:BufferedInputStream、BufferedOutputStream
区别:
项 | 字节流 | 字符流 |
---|---|---|
操作基本单元 | 字节 | 字符(Unicode码元) |
存在位置 | 可存在于文件、内存中。硬盘上的所有文件都是以字节形式存在的。 | 只存在于内存中。 |
使用场景 | 适合操作文本文件之外的文件。例:图片、音频、视频。 | 适合操作文本文件时使用。 |
Java相关类 | InputStream、OutputStream等。 | Reader、Writer等。 |
正确用法是:Java使用字符流读取文本文件,使用字节流读取非文本文件。
原因是:
- 字符流读文本文件时可以自动处理文件编码,确保正确地解析文件中的字符。
- 使用字符流读取非文本文件可能会导致一些问题。例如,字符流可能会将一些特定的字节序列(如0x0A)视为文件的行结尾,从而导致数据丢失。所以在读取这些文件时使用字节流比字符流更好。
2、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读取或者写入操作已经由内核完成了。 |
使用场景 | 适用于连接数目多且连接比较短(轻操作)的操作。例如:聊天服务器。 | 适用于连接数目多且连接比较长(重操作)的架构。例如:相册服务器。 |
BIO
实例
客户端
public class IOClient {
public static void main(String[] args) {
// TODO 创建多个线程,模拟多个客户端连接服务端
new Thread(() -> {
try {
Socket socket = new Socket("127.0.0.1", 3333);
while (true) {
try {
socket.getOutputStream().write((new Date() + ": hello world").getBytes());
Thread.sleep(2000);
} catch (Exception e) {
}
}
} catch (IOException e) {
}
}).start();
}
}
服务端
public class IOServer {
public static void main(String[] args) throws IOException {
// TODO 服务端处理客户端连接请求
ServerSocket serverSocket = new ServerSocket(3333);
// 接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理
new Thread(() -> {
while (true) {
try {
// 阻塞方法获取新的连接
Socket socket = serverSocket.accept();
// 每一个新的连接都创建一个线程,负责读取数据
new Thread(() -> {
try {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
// 按字节流方式读取数据
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
} catch (IOException e) {
}
}).start();
} catch (IOException e) {
}
}
}).start();
}
}
NIO
简介
JAVA NIO:新的IO(New I/O),其实是同一个概念。它是一种同步阻塞/非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。
NIO是一种基于通道和缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存(区别于JVM的运行时数据区),然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的直接引用进行操作。这样能在一些场景显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
实例
BIO部分的客户端 IOClient.java 的代码不变,我们对服务端使用 NIO 进行改造。
package org.example.a;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws IOException {
// 1. serverSelector负责轮询是否有新的连接,服务端监测到新的连接之后,不再创建一个新的线程,
// 而是直接将新连接绑定到clientSelector上,这样就不用 IO 模型中 1w 个 while 循环在死等
Selector serverSelector = Selector.open();
// 2. clientSelector负责轮询连接是否有数据可读
Selector clientSelector = Selector.open();
new Thread(() -> {
try {
// 对应IO编程中服务端启动
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(3333));
listenerChannel.configureBlocking(false);
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
while (true) {
// 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
if (serverSelector.select(1) > 0) {
Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
try {
// (1)每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {
keyIterator.remove();
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
new Thread(() -> {
try {
while (true) {
// (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
if (clientSelector.select(1) > 0) {
Set<SelectionKey> set = clientSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
try {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// (3) 面向 Buffer
clientChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(
Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());
} finally {
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
}
}
AIO
简介
AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。