本文收录于《面试小抄》系列,Github地址(可下载pdf):https://github.com/cosen1024/Java-Interview 国内Gitee(可下载pdf):https://gitee.com/cosen1024/Java-Interview
本文包含了String、包装类型、反射、泛型、序列化、异常和IO的常见面试题。
本文收录于《面试小抄》系列,Github地址:https://github.com/cosen1024/Java-Interview
包含了Java基础、Java并发、JVM、MySQL、Redis、Spring、MyBatis、Kafka、操作系统和计算机网络等的常考面试题。
坐稳,要发车了~
1. 字符型常量和字符串常量的区别?
形式上: 字符常量是单引号引起的一个字符,字符串常量是双引号引起的若干个字符;
含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算;字符串常量代表一个地址值(该字符串在内存中存放位置,相当于对象;
占内存大小:字符常量只占2个字节;字符串常量占若干个字节(至少一个字符结束标志) (注意: char 在Java中占两个字节)。
2. 什么是字符串常量池?
java中常量池的概念主要有三个:全局字符串常量池
,class文件常量池
,运行时常量池
。我们现在所说的就是全局字符串常量池
,对这个想弄明白的同学可以看这篇Java中几种常量池的区分。
jvm为了提升性能和减少内存开销,避免字符的重复创建,其维护了一块特殊的内存空间,即字符串池,当需要使用字符串时,先去字符串池中查看该字符串是否已经存在,如果存在,则可以直接使用,如果不存在,初始化,并将该字符串放入字符串常量池中。
字符串常量池的位置也是随着jdk版本的不同而位置不同。在jdk6中,常量池的位置在永久代(方法区)中,此时常量池中存储的是对象。在jdk7中,常量池的位置在堆中,此时,常量池存储的就是引用了。在jdk8中,永久代(方法区)被元空间取代了。
3. String str="aaa"与 String str=new String("aaa")一样吗?new String(“aaa”);
创建了几个字符串对象?
- 使用
String a = “aaa” ;
,程序运行时会在常量池中查找”aaa”字符串,若没有,会将”aaa”字符串放进常量池,再将其地址赋给a;若有,将找到的”aaa”字符串的地址赋给a。 - 使用String b = new String("aaa");`,程序会在堆内存中开辟一片新空间存放新对象,同时会将”aaa”字符串放入常量池,相当于创建了两个对象,无论常量池中有没有”aaa”字符串,程序都会在堆内存中开辟一片新空间存放新对象。
具体分析,见以下代码:
@Test
public void test(){
String s = new String("2");
s.intern();
String s2 = "2";
System.out.println(s == s2);
String s3 = new String("3") + new String("3");
s3.intern();
String s4 = "33";
System.out.println(s3 == s4);
}
运行结果:
jdk6
false
false
jdk7
false
true
这段代码在jdk6中输出是false false
,但是在jdk7中输出的是false true
。我们通过图来一行行解释。
先来认识下intern()函数:
intern函数的作用是将对应的符号常量进入特殊处理,在JDK1.6以前 和 JDK1.7以后有不同的处理;
在JDK1.6中,intern的处理是 先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量,如果没有找到,则将该字符串常量加入到字符串常量区,也就是在字符串常量区建立该常量;
在JDK1.7中,intern的处理是 先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量,如果没有找到,说明该字符串常量在堆中,则处理是把堆区该对象的引用加入到字符串常量池中,以后别人拿到的是该字符串常量的引用,实际存在堆中
JDK1.6
String s = new String("2");
创建了两个对象,一个在堆中的StringObject对象,一个是在常量池中的“2”对象。
s.intern();
在常量池中寻找与s变量内容相同的对象,发现已经存在内容相同对象“2”,返回对象2的地址。
String s2 = "2";
使用字面量创建,在常量池寻找是否有相同内容的对象,发现有,返回对象"2"的地址。
System.out.println(s == s2);
从上面可以分析出,s变量和s2变量地址指向的是不同的对象,所以返回false
String s3 = new String("3") + new String("3");
创建了两个对象,一个在堆中的StringObject对象,一个是在常量池中的“3”对象。中间还有2个匿名的new String("3")我们不去讨论它们。
s3.intern();
在常量池中寻找与s3变量内容相同的对象,没有发现“33”对象,在常量池中创建“33”对象,返回“33”对象的地址。
String s4 = "33";
使用字面量创建,在常量池寻找是否有相同内容的对象,发现有,返回对象"33"的地址。
System.out.println(s3 == s4);
从上面可以分析出,s3变量和s4变量地址指向的是不同的对象,所以返回false
JDK1.7
String s = new String("2");
创建了两个对象,一个在堆中的StringObject对象,一个是在堆中的“2”对象,并在常量池中保存“2”对象的引用地址。
s.intern();
在常量池中寻找与s变量内容相同的对象,发现已经存在内容相同对象“2”,返回对象“2”的引用地址。
String s2 = "2";
使用字面量创建,在常量池寻找是否有相同内容的对象,发现有,返回对象“2”的引用地址。
System.out.println(s == s2);
从上面可以分析出,s变量和s2变量地址指向的是不同的对象,所以返回false
String s3 = new String("3") + new String("3");
创建了两个对象,一个在堆中的StringObject对象,一个是在堆中的“3”对象,并在常量池中保存“3”对象的引用地址。中间还有2个匿名的new String("3")我们不去讨论它们。
s3.intern();
在常量池中寻找与s3变量内容相同的对象,没有发现“33”对象,将s3对应的StringObject对象的地址保存到常量池中,返回StringObject对象的地址。
String s4 = "33";
使用字面量创建,在常量池寻找是否有相同内容的对象,发现有,返回其地址,也就是StringObject对象的引用地址。
System.out.println(s3 == s4);
从上面可以分析出,s3变量和s4变量地址指向的是相同的对象,所以返回true。
4. String 是最基本的数据类型吗?
不是。Java 中的基本数据类型只有 8 个 :byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(referencetype),Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。
5. String有哪些特性?
不变性:String 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性;
常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用;
final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。
6. 在使用 HashMap 的时候,用 String 做 key 有什么好处?
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
7. 包装类型是什么?基本类型和包装类型有什么区别?
Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,把基本类型转换成包装类型的过程叫做装箱(boxing);反之,把包装类型转换成基本类型的过程叫做拆箱(unboxing),使得二者可以相互转换。
Java 为每个原始类型提供了包装类型:
原始类型: boolean,char,byte,short,int,long,float,double
包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
基本类型和包装类型的区别主要有以下 几点:
包装类型可以为 null,而基本类型不可以。它使得包装类型可以应用于 POJO 中,而基本类型则不行。那为什么 POJO 的属性必须要用包装类型呢?《阿里巴巴 Java 开发手册》上有详细的说明, 数据库的查询结果可能是 null,如果使用基本类型的话,因为要自动拆箱(将包装类型转为基本类型,比如说把 Integer 对象转换成 int 值),就会抛出
NullPointerException
的异常。-
包装类型可用于泛型,而基本类型不可以。泛型不能使用基本类型,因为使用基本类型时会编译出错。
List
list = new ArrayList<>(); // 提示 Syntax error, insert "Dimensions" to complete ReferenceType List list = new ArrayList<>(); 因为泛型在编译时会进行类型擦除,最后只保留原始类型,而原始类型只能是 Object 类及其子类——基本类型是个特例。
基本类型比包装类型更高效。基本类型在栈中直接存储的具体数值,而包装类型则存储的是堆中的引用。 很显然,相比较于基本类型而言,包装类型需要占用更多的内存空间。
8. 解释一下自动装箱和自动拆箱?
自动装箱:将基本数据类型重新转化为对象
public class Test {
public static void main(String[] args) {
// 声明一个Integer对象,用到了自动的装箱:解析为:Integer num = Integer.valueOf(9);
Integer num = 9;
}
}
9是属于基本数据类型的,原则上它是不能直接赋值给一个对象Integer的。但jdk1.5 开始引入了自动装箱/拆箱机制,就可以进行这样的声明,自动将基本数据类型转化为对应的封装类型,成为一个对象以后就可以调用对象所声明的所有的方法。
自动拆箱:将对象重新转化为基本数据类型
public class Test {
public static void main(String[] args) {
/ /声明一个Integer对象
Integer num = 9;
// 进行计算时隐含的有自动拆箱
System.out.print(num--);
}
}
因为对象时不能直接进行运算的,而是要转化为基本数据类型后才能进行加减乘除。
9. int 和 Integer 有什么区别?
- Integer是int的包装类;int是基本数据类型;
- Integer变量必须实例化后才能使用;int变量不需要;
- Integer实际是对象的引用,指向此new的Integer对象;int是直接存储数据值 ;
- Integer的默认值是null;int的默认值是0。
10. 两个new生成的Integer变量的对比
由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。
Integer i = new Integer(10000);
Integer j = new Integer(10000);
System.out.print(i == j); //false
11. Integer变量和int变量的对比
Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)
int a = 10000;
Integer b = new Integer(10000);
Integer c=10000;
System.out.println(a == b); // true
System.out.println(a == c); // true
12. 非new生成的Integer变量和new Integer()生成变量的对比
非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)
Integer b = new Integer(10000);
Integer c=10000;
System.out.println(b == c); // false
13. 两个非new生成的Integer对象的对比
对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false
Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true
Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false
当值在 -128 ~ 127之间时,java会进行自动装箱,然后会对值进行缓存,如果下次再有相同的值,会直接在缓存中取出使用。缓存是通过Integer的内部类IntegerCache来完成的。当值超出此范围,会在堆中new出一个对象来存储。
给一个Integer对象赋一个int值的时候,会调用Integer类的静态方法valueOf,源码如下:
public static Integer valueOf(String s, int radix) throws NumberFormatException {
return Integer.valueOf(parseInt(s,radix));
}
/**
* (1)在-128~127之内:静态常量池中cache数组是static final类型,cache数组对象会被存储于静态常量池中。
* cache数组里面的元素却不是static final类型,而是cache[k] = new Integer(j++),
* 那么这些元素是存储于堆中,只是cache数组对象存储的是指向了堆中的Integer对象(引用地址)
*
* (2)在-128~127 之外:新建一个 Integer对象,并返回。
*/
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high) {
return IntegerCache.cache[i + (-IntegerCache.low)];
}
return new Integer(i);
}
IntegerCache是Integer的内部类,源码如下:
/**
* 缓存支持自动装箱的对象标识语义 -128和127(含)。
* 缓存在第一次使用时初始化。 缓存的大小可以由-XX:AutoBoxCacheMax = 选项控制。
* 在VM初始化期间,java.lang.Integer.IntegerCache.high属性可以设置并保存在私有系统属性中
*/
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++) {
cache[k] = new Integer(j++); // 创建一个对象
}
}
private IntegerCache() {}
}
14. 什么是反射?
反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
15. 反射机制的优缺点有哪些?
优点:能够运行时动态获取类的实例,提高灵活性;可与动态编译结合Class.forName('com.mysql.jdbc.Driver.class');
,加载MySQL的驱动类。
缺点:使用反射性能较低,需要解析字节码,将内存中的对象进行解析。其解决方案是:通过setAccessible(true)关闭JDK的安全检查来提升反射速度;多次创建一个类的实例时,有缓存会快很多;ReflflectASM工具类,通过字节码生成的方式加快反射速度。
16. 如何获取反射中的Class对象?
-
Class.forName(“类的路径”);当你知道该类的全路径名时,你可以使用该方法获取 Class 类对象。
Class clz = Class.forName("java.lang.String");
-
类名.class。这种方法只适合在编译前就知道操作的 Class。
Class clz = String.class;
-
对象名.getClass()。
String str = new String("Hello"); Class clz = str.getClass();
如果是基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象。
17. Java反射API有几类?
反射 API 用来生成 JVM 中的类、接口或则对象的信息。
Class 类:反射的核心类,可以获取类的属性,方法等信息。
Field 类:Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。
Method 类:Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行方法。
Constructor 类:Java.lang.reflec 包中的类,表示类的构造方法。
18. 反射使用的步骤?
获取想要操作的类的Class对象,这是反射的核心,通过Class对象我们可以任意调用类的方法。
调用 Class 类中的方法,既就是反射的使用阶段。
使用反射 API 来操作这些信息。
具体可以看下面的例子:
public class Apple {
private int price;
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public static void main(String[] args) throws Exception{
//正常的调用
Apple apple = new Apple();
apple.setPrice(5);
System.out.println("Apple Price:" + apple.getPrice());
//使用反射调用
Class clz = Class.forName("com.chenshuyi.api.Apple");
Method setPriceMethod = clz.getMethod("setPrice", int.class);
Constructor appleConstructor = clz.getConstructor();
Object appleObj = appleConstructor.newInstance();
setPriceMethod.invoke(appleObj, 14);
Method getPriceMethod = clz.getMethod("getPrice");
System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj));
}
}
从代码中可以看到我们使用反射调用了 setPrice 方法,并传递了 14 的值。之后使用反射调用了 getPrice 方法,输出其价格。上面的代码整个的输出结果是:
Apple Price:5
Apple Price:14
从这个简单的例子可以看出,一般情况下我们使用反射获取一个对象的步骤:
- 获取类的 Class 对象实例
Class clz = Class.forName("com.zhenai.api.Apple");
- 根据 Class 对象实例获取 Constructor 对象
Constructor appleConstructor = clz.getConstructor();
- 使用 Constructor 对象的 newInstance 方法获取反射类对象
Object appleObj = appleConstructor.newInstance();
而如果要调用某一个方法,则需要经过下面的步骤:
- 获取方法的 Method 对象
Method setPriceMethod = clz.getMethod("setPrice", int.class);
- 利用 invoke 方法调用方法
setPriceMethod.invoke(appleObj, 14);
19. 为什么引入反射概念?反射机制的应用有哪些?
我们来看一下 Oracle 官方文档中对反射的描述:
从 Oracle 官方文档中可以看出,反射主要应用在以下几方面:
- 反射让开发人员可以通过外部类的全路径名创建对象,并使用这些类,实现一些扩展的功能。
- 反射让开发人员可以枚举出类的全部成员,包括构造函数、属性、方法。以帮助开发者写出正确的代码。
- 测试时可以利用反射 API 访问类的私有成员,以保证测试代码覆盖率。
也就是说,Oracle 希望开发者将反射作为一个工具,用来帮助程序员实现本不可能实现的功能。
举两个最常见使用反射的例子,来说明反射机制的强大之处:
第一种:JDBC 的数据库的连接
在JDBC 的操作中,如果要想进行数据库的连接,则必须按照以上的几步完成
- 通过Class.forName()加载数据库的驱动程序 (通过反射加载,前提是引入相关了Jar包);
- 通过 DriverManager 类进行数据库的连接,连接的时候要输入数据库的连接地址、用户名、密码;
- 通过Connection 接口接收连接。
public class ConnectionJDBC {
/**
* @param args
*/
//驱动程序就是之前在classpath中配置的JDBC的驱动程序的JAR 包中
public static final String DBDRIVER = "com.mysql.jdbc.Driver";
//连接地址是由各个数据库生产商单独提供的,所以需要单独记住
public static final String DBURL = "jdbc:mysql://localhost:3306/test";
//连接数据库的用户名
public static final String DBUSER = "root";
//连接数据库的密码
public static final String DBPASS = "";
public static void main(String[] args) throws Exception {
Connection con = null; //表示数据库的连接对象
Class.forName(DBDRIVER); //1、使用CLASS 类加载驱动程序 ,反射机制的体现
con = DriverManager.getConnection(DBURL,DBUSER,DBPASS); //2、连接数据库
System.out.println(con);
con.close(); // 3、关闭数据库
}
第二种:Spring 框架的使用,最经典的就是xml的配置模式。
Spring 通过 XML 配置模式装载 Bean 的过程:
- 将程序内所有 XML 或 Properties 配置文件加载入内存中;
- Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;
- 使用反射机制,根据这个字符串获得某个类的Class实例;
- 动态配置实例的属性。
Spring这样做的好处是:
- 不用每一次都要在代码里面去new或者做其他的事情;
- 以后要改的话直接改配置文件,代码维护起来就很方便了;
- 有时为了适应某些需求,Java类里面不一定能直接调用另外的方法,可以通过反射机制来实现。
模拟 Spring 加载 XML 配置文件:
public class BeanFactory {
private Map beanMap = new HashMap();
/**
* bean工厂的初始化.
* @param xml xml配置文件
*/
public void init(String xml) {
try {
//读取指定的配置文件
SAXReader reader = new SAXReader();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
//从class目录下获取指定的xml文件
InputStream ins = classLoader.getResourceAsStream(xml);
Document doc = reader.read(ins);
Element root = doc.getRootElement();
Element foo;
//遍历bean
for (Iterator i = root.elementIterator("bean"); i.hasNext();) {
foo = (Element) i.next();
//获取bean的属性id和class
Attribute id = foo.attribute("id");
Attribute cls = foo.attribute("class");
//利用Java反射机制,通过class的名称获取Class对象
Class bean = Class.forName(cls.getText());
//获取对应class的信息
java.beans.BeanInfo info = java.beans.Introspector.getBeanInfo(bean);
//获取其属性描述
java.beans.PropertyDescriptor pd[] = info.getPropertyDescriptors();
//设置值的方法
Method mSet = null;
//创建一个对象
Object obj = bean.newInstance();
//遍历该bean的property属性
for (Iterator ite = foo.elementIterator("property"); ite.hasNext();) {
Element foo2 = (Element) ite.next();
//获取该property的name属性
Attribute name = foo2.attribute("name");
String value = null;
//获取该property的子元素value的值
for(Iterator ite1 = foo2.elementIterator("value"); ite1.hasNext();) {
Element node = (Element) ite1.next();
value = node.getText();
break;
}
for (int k = 0; k < pd.length; k++) {
if (pd[k].getName().equalsIgnoreCase(name.getText())) {
mSet = pd[k].getWriteMethod();
//利用Java的反射极致调用对象的某个set方法,并将值设置进去
mSet.invoke(obj, value);
}
}
}
//将对象放入beanMap中,其中key为id值,value为对象
beanMap.put(id.getText(), obj);
}
} catch (Exception e) {
System.out.println(e.toString());
}
}
//other codes
}
20. 反射机制的原理是什么?
Class actionClass=Class.forName(“MyClass”);
Object action=actionClass.newInstance();
Method method = actionClass.getMethod(“myMethod”,null);
method.invoke(action,null);
上面就是最常见的反射使用的例子,前两行实现了类的装载、链接和初始化(newInstance方法实际上也是使用反射调用了
因反射原理较复杂,下面简要描述下流程,想要详细了解的小伙伴,可以看这篇文章:https://www.cnblogs.com/yougewe/p/10125073.html
反射获取类实例 Class.forName(),并没有将实现留给了java,而是交给了jvm去加载!主要是先获取 ClassLoader, 然后调用 native 方法,获取信息,加载类则是回调 java.lang.ClassLoader。最后,jvm又会回调 ClassLoader 进类加载!
-
newInstance() 主要做了三件事:
- 权限检测,如果不通过直接抛出异常;
- 查找无参构造器,并将其缓存起来;
- 调用具体方法的无参构造方法,生成实例并返回。
-
获取Method对象,
上面的Class对象是在加载类时由JVM构造的,JVM为每个类管理一个独一无二的Class对象,这份Class对象里维护着该类的所有Method,Field,Constructor的cache,这份cache也可以被称作根对象。
每次getMethod获取到的Method对象都持有对根对象的引用,因为一些重量级的Method的成员变量(主要是MethodAccessor),我们不希望每次创建Method对象都要重新初始化,于是所有代表同一个方法的Method对象都共享着根对象的MethodAccessor,每一次创建都会调用根对象的copy方法复制一份:
Method copy() {
Method res = new Method(clazz, name, parameterTypes, returnType,
exceptionTypes, modifiers, slot, signature,
annotations, parameterAnnotations, annotationDefault);
res.root = this;
res.methodAccessor = methodAccessor;
return res;
}
-
调用invoke()方法。调用invoke方法的流程如下:
调用Method.invoke之后,会直接去调MethodAccessor.invoke。MethodAccessor就是上面提到的所有同名method共享的一个实例,由ReflectionFactory创建。
创建机制采用了一种名为inflation的方式(JDK1.4之后):如果该方法的累计调用次数<=15,会创建出NativeMethodAccessorImpl,它的实现就是直接调用native方法实现反射;如果该方法的累计调用次数>15,会由java代码创建出字节码组装而成的MethodAccessorImpl。(是否采用inflation和15这个数字都可以在jvm参数中调整)
以调用MyClass.myMethod(String s)为例,生成出的MethodAccessorImpl字节码翻译成Java代码大致如下:
public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
public Object invoke(Object obj, Object[] args) throws Exception {
try {
MyClass target = (MyClass) obj;
String arg0 = (String) args[0];
target.myMethod(arg0);
} catch (Throwable t) {
throw new InvocationTargetException(t);
}
}
}
21. Java中的泛型是什么 ?
泛型是 JDK1.5 的一个新特性,泛型就是将类型参数化,其在编译时才确定具体的参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
22. 使用泛型的好处是什么?
远在 JDK 1.4 版本的时候,那时候是没有泛型的概念的,如果使用 Object 来实现通用、不同类型的处理,有这么两个缺点:
- 每次使用时都需要强制转换成想要的类型
- 在编译时编译器并不知道类型转换是否正常,运行时才知道,不安全。
如这个例子:
List list = new ArrayList();
list.add("www.cnblogs.com");
list.add(23);
String name = (String)list.get(0);
String number = (String)list.get(1); //ClassCastException
上面的代码在运行时会发生强制类型转换异常。这是因为我们在存入的时候,第二个是一个 Integer 类型,但是取出来的时候却将其强制转换为 String 类型了。Sun 公司为了使 Java 语言更加安全,减少运行时异常的发生。于是在 JDK 1.5 之后推出了泛型的概念。
根据《Java 编程思想》中的描述,泛型出现的动机在于:有许多原因促成了泛型的出现,而最引人注意的一个原因,就是为了创建容器类。
使用泛型的好处有以下几点:
-
类型安全
- 泛型的主要目标是提高 Java 程序的类型安全
- 编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常
- 符合越早出错代价越小原则
-
消除强制类型转换
- 泛型的一个附带好处是,使用时直接得到目标类型,消除许多强制类型转换
- 所得即所需,这使得代码更加可读,并且减少了出错机会
-
潜在的性能收益
- 由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改
- 所有工作都在编译器中完成
- 编译器生成的代码跟不使用泛型(和强制类型转换)时所写的代码几乎一致,只是更能确保类型安全而已
23. Java泛型的原理是什么 ? 什么是类型擦除 ?
泛型是一种语法糖,泛型这种语法糖的基本原理是类型擦除。Java中的泛型基本上都是在编译器这个层次来实现的,也就是说:泛型只存在于编译阶段,而不存在于运行阶段。在编译后的 class 文件中,是没有泛型这个概念的。
类型擦除:使用泛型的时候加上的类型参数,编译器在编译的时候去掉类型参数。
例如:
public class Caculate {
private T num;
}
我们定义了一个泛型类,定义了一个属性成员,该成员的类型是一个泛型类型,这个 T 具体是什么类型,我们也不知道,它只是用于限定类型的。反编译一下这个 Caculate 类:
public class Caculate{
public Caculate(){}
private Object num;
}
发现编译器擦除 Caculate 类后面的两个尖括号,并且将 num 的类型定义为 Object 类型。
那么是不是所有的泛型类型都以 Object 进行擦除呢?大部分情况下,泛型类型都会以 Object 进行替换,而有一种情况则不是。那就是使用到了extends和super语法的有界类型,如:
public class Caculate {
private T num;
}
这种情况的泛型类型,num 会被替换为 String 而不再是 Object。这是一个类型限定的语法,它限定 T 是 String 或者 String 的子类,也就是你构建 Caculate 实例的时候只能限定 T 为 String 或者 String 的子类,所以无论你限定 T 为什么类型,String 都是父类,不会出现类型不匹配的问题,于是可以使用 String 进行类型擦除。
实际上编译器会正常的将使用泛型的地方编译并进行类型擦除,然后返回实例。但是除此之外的是,如果构建泛型实例时使用了泛型语法,那么编译器将标记该实例并关注该实例后续所有方法的调用,每次调用前都进行安全检查,非指定类型的方法都不能调用成功。
实际上编译器不仅关注一个泛型方法的调用,它还会为某些返回值为限定的泛型类型的方法进行强制类型转换,由于类型擦除,返回值为泛型类型的方法都会擦除成 Object 类型,当这些方法被调用后,编译器会额外插入一行 checkcast 指令用于强制类型转换。这一个过程就叫做『泛型翻译』。
24. 什么是泛型中的限定通配符和非限定通配符 ?
限定通配符对类型进行了限制。有两种限定通配符,一种是 extends T>它通过确保类型必须是T的子类来设定类型的上界,另一种是 super T>它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。
非限定通配符 ?,可以用任意类型来替代。如List>
的意思是这个集合是一个可以持有任意类型的集合,它可以是List
,也可以是List
,或者List
等等。
25. List extends T>和List super T>之间有什么区别 ?
这两个List的声明都是限定通配符的例子,List extends T>可以接受任何继承自T的类型的List,而List super T>可以接受任何T的父类构成的List。例如List extends Number>可以接受List
26. 可以把List
传递给一个接受List
参数的方法吗?
不可以。真这样做的话会导致编译错误。因为List
List
27. 判断ArrayList
与ArrayList
是否相等?
ArrayList a = new ArrayList();
ArrayList b = new ArrayList();
Class c1 = a.getClass();
Class c2 = b.getClass();
System.out.println(c1 == c2);
输出的结果是 true。因为无论对于 ArrayList 还是 ArrayList,它们的 Class 类型都是一直的,都是 ArrayList.class。
那它们声明时指定的 String 和 Integer 到底体现在哪里呢?
答案是体现在类编译的时候。当 JVM 进行类编译时,会进行泛型检查,如果一个集合被声明为 String 类型,那么它往该集合存取数据的时候就会对数据进行判断,从而避免存入或取出错误的数据。
补充: Array中可以用泛型吗?
不可以。这也是为什么 Joshua Bloch 在 《Effective Java》一书中建议使用 List 来代替 Array,因为 List 可以提供编译期的类型安全保证,而 Array 却不能。
28. Java序列化与反序列化是什么?
Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程:
-
序列化:序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。核心作用是对象状态的保存与重建。我们都知道,Java对象是保存在JVM的堆内存中的,也就是说,如果JVM堆不存在了,那么对象也就跟着消失了。
而序列化提供了一种方案,可以让你在即使JVM停机的情况下也能把对象保存下来的方案。就像我们平时用的U盘一样。把Java对象序列化成可存储或传输的形式(如二进制流),比如保存在文件中。这样,当再次需要这个对象的时候,从文件中读取出二进制流,再从二进制流中反序列化出对象。
反序列化:客户端从文件中或网络上获得序列化后的对象字节流,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
29. 为什么需要序列化与反序列化?
简要描述:对内存中的对象进行持久化或网络传输, 这个时候都需要序列化和反序列化
深入描述:
- 对象序列化可以实现分布式对象。
主要应用例如:RMI(即远程调用Remote Method Invocation)要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。
- java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。
可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的"深复制",即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。
- 序列化可以将内存中的类写入文件或数据库中。
比如:将某个类序列化后存为文件,下次读取时只需将文件中的数据反序列化就可以将原先的类还原到内存中。也可以将类序列化为流数据进行传输。
总的来说就是将一个已经实例化的类转成文件存储,下次需要实例化的时候只要反序列化即可将类实例化到内存中并保留序列化时类中的所有变量和状态。
- 对象、文件、数据,有许多不同的格式,很难统一传输和保存。
序列化以后就都是字节流了,无论原来是什么东西,都能变成一样的东西,就可以进行通用的格式传输或保存,传输结束以后,要再次使用,就进行反序列化还原,这样对象还是对象,文件还是文件。
30. 序列化实现的方式有哪些?
实现Serializable接口或者Externalizable接口。
Serializable接口
类通过实现 java.io.Serializable
接口以启用其序列化功能。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。
如以下例子:
import java.io.Serializable;
public class User implements Serializable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name +
'}';
}
}
通过下面的代码进行序列化及反序列化:
public class SerializableDemo {
public static void main(String[] args) {
//Initializes The Object
User user = new User();
user.setName("cosen");
System.out.println(user);
//Write Obj to File
try (FileOutputStream fos = new FileOutputStream("tempFile"); ObjectOutputStream oos = new ObjectOutputStream(
fos)) {
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
}
//Read Obj from File
File file = new File("tempFile");
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
User newUser = (User)ois.readObject();
System.out.println(newUser);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
//OutPut:
//User{name='cosen'}
//User{name='cosen'}
Externalizable接口
Externalizable
继承自Serializable
,该接口中定义了两个抽象方法:writeExternal()
与readExternal()
。
当使用Externalizable
接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()
与readExternal()
方法。否则所有变量的值都会变成默认值。
public class User implements Externalizable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
}
@Override
public String toString() {
return "User{" +
"name='" + name +
'}';
}
}
通过下面的代码进行序列化及反序列化:
public class ExternalizableDemo1 {
public static void main(String[] args) {
//Write Obj to file
User user = new User();
user.setName("cosen");
try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"))){
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
}
//Read Obj from file
File file = new File("tempFile");
try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))){
User newInstance = (User) ois.readObject();
//output
System.out.println(newInstance);
} catch (IOException | ClassNotFoundException e ) {
e.printStackTrace();
}
}
}
//OutPut:
//User{name='cosen'}
两种序列化的对比
实现Serializable接口 | 实现Externalizable接口 |
---|---|
系统自动存储必要的信息 | 程序员决定存储哪些信息 |
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 | 必须实现接口内的两个方法 |
性能略差 | 性能略好 |
31. 什么是serialVersionUID?
serialVersionUID 用来表明类的不同版本间的兼容性
Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。
32. 为什么还要显示指定serialVersionUID的值?
如果不显示指定serialVersionUID, JVM在序列化时会根据属性自动生成一个serialVersionUID, 然后与属性一起序列化, 再进行持久化或网络传输. 在反序列化时, JVM会再根据属性自动生成一个新版serialVersionUID, 然后将这个新版serialVersionUID与序列化时生成的旧版serialVersionUID进行比较, 如果相同则反序列化成功, 否则报错.
如果显示指定了, JVM在序列化和反序列化时仍然都会生成一个serialVersionUID, 但值为我们显示指定的值, 这样在反序列化时新旧版本的serialVersionUID就一致了.
在实际开发中, 不显示指定serialVersionUID的情况会导致什么问题? 如果我们的类写完后不再修改, 那当然不会有问题, 但这在实际开发中是不可能的, 我们的类会不断迭代, 一旦类被修改了, 那旧对象反序列化就会报错. 所以在实际开发中, 我们都会显示指定一个serialVersionUID, 值是多少无所谓, 只要不变就行。
33. serialVersionUID什么时候修改?
《阿里巴巴Java开发手册》中有以下规定:
想要深入了解的小伙伴,可以看这篇文章:https://juejin.cn/post/6844903746682486791
34. Java 序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用 transient 关键字修饰。
transient
关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient
变量的值被设为初始值,如 int 型的是 0,对象型的是 null。transient 只能修饰变量,不能修饰类和方法。
35. 静态变量会被序列化吗?
不会。因为序列化是针对对象而言的, 而静态变量优先于对象存在, 随着类的加载而加载, 所以不会被序列化.
看到这个结论, 是不是有人会问, serialVersionUID也被static修饰, 为什么serialVersionUID会被序列化? 其实serialVersionUID属性并没有被序列化, JVM在序列化对象时会自动生成一个serialVersionUID, 然后将我们显示指定的serialVersionUID属性值赋给自动生成的serialVersionUID。
36. Error 和 Exception 区别是什么?
Java 中,所有的异常都有一个共同的祖先 java.lang
包中的 Throwable
类。Throwable
类有两个重要的子类 Exception
(异常)和 Error
(错误)。
Exception
和 Error
二者都是 Java 异常处理的重要子类,各自都包含大量子类。
-
Exception
:程序本身可以处理的异常,可以通过catch
来进行捕获,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。Exception
又可以分为运行时异常(RuntimeException, 又叫非受检查异常)和非运行时异常(又叫受检查异常) 。 -
Error
:Error
属于程序无法处理的错误 ,我们没办法通过catch
来进行捕获 。例如,系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复。
37. 非受检查异常(运行时异常)和受检查异常(一般异常)区别是什么?
非受检查异常:包括 RuntimeException
类及其子类,表示 JVM 在运行期间可能出现的异常。 Java 编译器不会检查运行时异常。例如:NullPointException(空指针)
、NumberFormatException(字符串转换为数字)
、IndexOutOfBoundsException(数组越界)
、ClassCastException(类转换异常)
、ArrayStoreException(数据存储异常,操作数组时类型不一致)
等。
受检查异常:是Exception 中除 RuntimeException
及其子类之外的异常。 Java 编译器会检查受检查异常。常见的受检查异常有: IO 相关的异常、ClassNotFoundException
、SQLException
等。
非受检查异常和受检查异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检查异常,否则就选择非受检查异常。
38. throw 和 throws 的区别是什么?
Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。
throws 关键字和 throw 关键字在使用上的几点区别如下:
- throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。
- throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。
举例如下:
throw 关键字:
public static void main(String[] args) {
String s = "abc";
if(s.equals("abc")) {
throw new NumberFormatException();
} else {
System.out.println(s);
}
//function();
}
throws 关键字:
public static void function() throws NumberFormatException{
String s = "abc";
System.out.println(Double.parseDouble(s));
}
public static void main(String[] args) {
try {
function();
} catch (NumberFormatException e) {
System.err.println("非数据类型不能转换。");
//e.printStackTrace();
}
}
39. NoClassDefFoundError 和 ClassNotFoundException 区别?
NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致。
ClassNotFoundException 是一个受检查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。
40. Java常见异常有哪些?
- java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。
- java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常.
- java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。
- java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。
- java.lang.ClassCastException:类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。
- java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
- java.lang.ArithmeticException:算术条件异常。譬如:整数除零等。
- java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
- java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。
- java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。
- java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。
- java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。
- java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。
- java.lang.NumberFormatException:数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。
- java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。
41. try-catch-finally 中哪个部分可以省略?
catch 可以省略。更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运行时异常+普通异常。也就是说,如果你只用try去处理普通异常却不加以catch处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略,你加上catch编译器也觉得无可厚非。
理论上,编译器看任何代码都不顺眼,都觉得可能有潜在的问题,所以你即使对所有代码加上try,代码在运行期时也只不过是在正常运行的基础上加一层皮。但是你一旦对一段代码加上try,就等于显示地承诺编译器,对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须用catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾处理,或者加上catch捕获以便进一步处理。
至于加上finally,则是在不管有没捕获异常,都要进行的“扫尾”处理。
42. try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?
会执行,在 return 前执行。
在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,Java 中也可以通过提升编译器的语法检查级别来产生警告或错误。
代码示例1:
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
/*
* return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了
* 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40
* 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30
*/
} finally {
a = 40;
}
return a;
}
//执行结果:30
代码示例2:
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
} finally {
a = 40;
//如果这样,就又重新形成了一条返回路径,由于只能通过1个return返回,所以这里直接返回40
return a;
}
}
// 执行结果:40
43. JVM 是如何处理异常的?
在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。
想要深入了解的小伙伴可以看这篇文章:https://www.cnblogs.com/qdhxhz/p/10765839.html
44. Java的IO 流分为几种?
- 按照流的方向:输入流(inputStream)和输出流(outputStream);
- 按照实现功能分:节点流(可以从或向一个特定的地方读写数据,如 FileReader)和处理流(是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写, BufferedReader);
- 按照处理数据的单位: 字节流和字符流。分别由四个抽象类来表示(每种流包括输入和输出两种所以一共四个):InputStream,OutputStream,Reader,Writer。Java中其他多种多样变化的流均是由它们派生出来的。
45. 字节流如何转为字符流?
字节输入流转字符输入流通过 InputStreamReader 实现,该类的构造函数可以传入 InputStream 对象。
字节输出流转字符输出流通过 OutputStreamWriter 实现,该类的构造函数可以传入 OutputStream 对象。
46. 字符流与字节流的区别?
- 读写的时候字节流是按字节读写,字符流按字符读写。
- 字节流适合所有类型文件的数据传输,因为计算机字节(Byte)是电脑中表示信息含义的最小单位。字符流只能够处理纯文本数据,其他类型数据不行,但是字符流处理文本要比字节流处理文本要方便。
- 在读写文件需要对内容按行处理,比如比较特定字符,处理某一行数据的时候一般会选择字符流。
- 只是读写文件,和文件内容无关时,一般选择字节流。
47. 什么是阻塞IO?什么是非阻塞IO?
IO操作包括:对硬盘的读写、对socket的读写以及外设的读写。
当用户线程发起一个IO请求操作(本文以读请求操作为例),内核会去查看要读取的数据是否就绪,对于阻塞IO来说,如果数据没有就绪,则会一直在那等待,直到数据就绪;对于非阻塞IO来说,如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪。当数据就绪之后,便将数据拷贝到用户线程,这样才完成了一个完整的IO读请求操作,也就是说一个完整的IO读请求操作包括两个阶段:
1)查看数据是否就绪;
2)进行数据拷贝(内核将数据拷贝到用户线程)。
那么阻塞(blocking IO)和非阻塞(non-blocking IO)的区别就在于第一个阶段,如果数据没有就绪,在查看数据是否就绪的过程中是一直等待,还是直接返回一个标志信息。
Java中传统的IO都是阻塞IO,比如通过socket来读数据,调用read()方法之后,如果数据没有就绪,当前线程就会一直阻塞在read方法调用那里,直到有数据才返回;而如果是非阻塞IO的话,当数据没有就绪,read()方法应该返回一个标志信息,告知当前线程数据没有就绪,而不是一直在那里等待。
深入了解可看这篇文章:https://mp.weixin.qq.com/s/p5qM2UJ1uIWyongfVpRbCg
48. BIO、NIO、AIO的区别?
- BIO:同步并阻塞,在服务器中实现的模式为一个连接一个线程。也就是说,客户端有连接请求的时候,服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然这也可以通过线程池机制改善。BIO一般适用于连接数目小且固定的架构,这种方式对于服务器资源要求比较高,而且并发局限于应用中,是JDK1.4之前的唯一选择,但好在程序直观简单,易理解。
- NIO:同步并非阻塞,在服务器中实现的模式为一个请求一个线程,也就是说,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到有连接IO请求时才会启动一个线程进行处理。NIO一般适用于连接数目多且连接比较短(轻操作)的架构,并发局限于应用中,编程比较复杂,从JDK1.4开始支持。
- AIO:异步并非阻塞,在服务器中实现的模式为一个有效请求一个线程,也就是说,客户端的IO请求都是通过操作系统先完成之后,再通知服务器应用去启动线程进行处理。AIO一般适用于连接数目多且连接比较长(重操作)的架构,充分调用操作系统参与并发操作,编程比较复杂,从JDK1.7开始支持。
49. Java IO都有哪些设计模式?
使用了适配器模式和装饰器模式
适配器模式:
Reader reader = new INputStreamReader(inputStream);
把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作
- 类适配器:Adapter类(适配器)继承Adaptee类(源角色)实现Target接口(目标角色)
- 对象适配器:Adapter类(适配器)持有Adaptee类(源角色)对象实例,实现Target接口(目标角色)
装饰器模式:
new BufferedInputStream(new FileInputStream(inputStream));
一种动态地往一个类中添加新的行为的设计模式。就功能而言,装饰器模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。
- ConcreteComponent(具体对象)和Decorator(抽象装饰器)实现相同的Conponent(接口)并且Decorator(抽象装饰器)里面持有Conponent(接口)对象,可以传递请求。
- ConcreteComponent(具体装饰器)覆盖Decorator(抽象装饰器)的方法并用super进行调用,传递请求。