Java基础常见面试题总结下
异常
Java异常类层次结构概览:
Exception和Error有什么区别?
二者都有一个共同的祖先:java.lang包中的Throwable类。
Throwable类中有两个重要的子类:
-
Exception:程序本身可以处理的异常,可以通过catch捕获。Exception又可以分为Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
-
Error:error属于程序无法处理的错误,不建议通过catch来捕获。例如例如 Java 虚拟机运行错误(
Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等,这些异常发生时,jvm一般会选择线程终止。
Checked Exception和Uncheked Exception有什么区别?
-
Checked Exception:受检查异常,若受检查异常没有被捕获或者抛出的话,就没办法通过编译。常见的受检查异常:IO相关的异常、
ClassNotFoundException
、SQLException
...。 -
Uncheked Exception:不受检查异常,编译过程中,即使不处理不受检查异常也可以正常通过。常见的有:
NullPointerException
(空指针错误),ArrayIndexOutOfBoundsException
(数组越界错误)
Throwable类常用的方法有哪些?
String toString()
: 返回异常发生时的简要描述
String getMessage()
: 返回异常发生时的详细信息
String getLocalizedMessage()
: 返回异常对象的本地化信息。使用 Throwable
的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()
返回的结果相同
void printStackTrace()
: 在控制台上打印 Throwable
对象封装的异常信息
try-catch-finally如何使用?
-
try:捕获异常,后面可以写多个或者不写catch,如果不写那就必须写finally。
-
catch:用于处理try捕获的异常
-
finally:如果catch没有捕获或者处理异常,那就执行finally中的语句。
注意:当在try和catch中遇到return语句时,finally中的内容将在方法返回前被执行。
try { System.out.println("Try to do something"); throw new RuntimeException("RuntimeException"); } catch (Exception e) { System.out.println("Catch Exception -> " + e.getMessage()); } finally { System.out.println("Finally"); }
输出:
Try to do something Catch Exception -> RuntimeException Finally
不要在finally中使用return,因为当try和return语句中都有return语句时,try语句块中的return语句会被忽略,因为try中的return会被存储在一个本地变量中,当执行到finally语句的return之后,这个本地变量会被finally的return中的覆盖。
public static void main(String[] args) { System.out.println(f(2)); } public static int f(int value) { try { return value * value; } finally { if (value == 2) { return 0; } } }
输出:
0
finally中的代码一定会执行吗?
不一定!比如finally之前虚拟机停止运行,finally中的代码就不会被执行。
try { System.out.println("Try to do something"); throw new RuntimeException("RuntimeException"); } catch (Exception e) { System.out.println("Catch Exception -> " + e.getMessage()); // 终止当前正在运行的Java虚拟机 System.exit(1); } finally { System.out.println("Finally"); }
输出:
Try to do something Catch Exception -> RuntimeException
另外,在以下 2 种特殊情况下,finally
块的代码也不会被执行:
-
程序所在的线程死亡。
-
关闭 CPU。
如何使用try-with-resource代替try-catch-finally?
-
适用范围(资源的定义):任何实现
java.lang.AutoCloseable
或者java.io.Closeable
的对象。 -
关闭资源和finally块的执行顺序:在try-with-resource语句中,catch块和finally块必须要等声明的资源关闭了才能执行。
面对必须要关闭的资源,应该优先使用
try-with-resources
而不是try-finally
。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources
语句让我们更容易编写必须要关闭的资源的代码,若采用try-finally
则几乎做不到这点。
Java 中类似于InputStream
、OutputStream
、Scanner
、PrintWriter
等的资源都需要我们调用close()
方法来手动关闭,一般情况下我们都是通过try-catch-finally
语句来实现这个需求,如下:
//读取文本文件的内容 Scanner scanner = null; try { scanner = new Scanner(new File("D://read.txt")); while (scanner.hasNext()) { System.out.println(scanner.nextLine()); } } catch (FileNotFoundException e) { e.printStackTrace(); } finally { if (scanner != null) { scanner.close(); } }
使用 Java 7 之后的 try-with-resources
语句改造上面的代码:
try (Scanner scanner = new Scanner(new File("test.txt"))) { while (scanner.hasNext()) { System.out.println(scanner.nextLine()); } } catch (FileNotFoundException fnfe) { fnfe.printStackTrace(); }
当然多个资源需要关闭的时候,使用 try-with-resources
实现起来也非常简单,如果你还是用try-catch-finally
可能会带来很多问题。
通过使用分号分隔,可以在try-with-resources
块中声明多个资源。
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt"))); BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) { int b; while ((b = bin.read()) != -1) { bout.write(b); } } catch (IOException e) { e.printStackTrace(); }
总结:将必须要关闭的资源写到try后面的小括号中,就会自动关闭,无需我们手动在finally中手动释放。
异常使用有哪些需要注意的地方?
-
不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
-
抛出的异常信息一定要有意义。
-
建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出
NumberFormatException
而不是其父类IllegalArgumentException
。
泛型
JDK5引入的新参数,使用泛型,可以增强代码的可读性以及稳定性。
编译器会对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如:ArrayList<Person> persons = new ArrayList<Person>()
这行代码就指明了该 ArrayList
对象只能传入 Person
对象,如果传入其他类型的对象就会报错。
泛型的使用方式有哪几种?
一般有三种:泛型类,泛型接口,泛型方法
1.泛型类
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 //在实例化泛型类时,必须指定T的具体类型 public class Generic<T>{ private T key; public Generic(T key) { this.key = key; } public T getKey(){ return key; } }
如何实例化泛型类:
Generic<Integer> genericInteger = new Generic<Integer>(123456);
2.泛型接口
public interface Generator<T> { public T method(); }
实现泛型接口,不指定类型:
class GeneratorImpl<T> implements Generator<T>{ @Override public T method() { return null; } }
实现泛型接口,指定类型:
class GeneratorImpl implements Generator<String> { @Override public String method() { return "hello"; } }
3.泛型方法
public static < E > void printArray( E[] inputArray ) { for ( E element : inputArray ){ System.out.printf( "%s ", element ); } System.out.println(); }
使用:
// 创建不同类型数组:Integer, Double 和 Character Integer[] intArray = { 1, 2, 3 }; String[] stringArray = { "Hello", "World" }; printArray( intArray ); printArray( stringArray );
注意:
public static < E > void printArray( E[] inputArray )
一般被称为静态泛型方法;在 java 中泛型只是一个占位符,必须在传递类型后才能使用。类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的。只能使用自己声明的<E>
项目中哪里用到了泛型?
-
自定义接口通用返回结果
CommonResult<T>
通过参数T
可根据具体的返回类型动态指定结果的数据类型
/** * 全局统一返回结果类 */ @Data public class Result<T> { //返回码 private Integer code; //返回消息 private String message; //返回数据 private T data; public Result() { } private static <T> Result<T> build(T data) { Result<T> result = new Result<>(); if (data != null) result.setData(data); return result; }
-
构建集合工具类(参考
Collections
中的sort
,binarySearch
方法)。
反射
可以参考:mxbb反射总结
Java Reflection: Why is it so slow?
注解
注解是什么?
anotation注解是java5引入的新特性,可以看做是一种特殊的注释,主要用于修饰类,方法或者变量,提供某些信息程序在编译或者运行时使用。
注解的本质是一个继承了annotation的特殊接口:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { } public interface Override extends Annotation{ }
JDK提供很多类似于@Override、
@Deprecated的内置注解,同时我们还可以自定义注解。
注解解析的方法有哪几种?
注解只有被解析之后才会生效,常见的解析方法有两种:
-
编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用
@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 -
运行期通过反射处理:像框架中自带的注解(如spring的@value,@component)都是通过反射来进行处理的。
语法糖
一种糖衣语法,旨在简化开发,但是功能不变。语法糖主要是给开发人员使用,因为JVM并不会识别语法糖,这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖desuger().
常见的Java语法糖
1.switch支持string
对于编译器来说,switch中其实只能使用整型,任何类型的比较都要转换成整型。
public class switchDemoString { public static void main(String[] args) { String str = "world"; switch (str) { case "hello": System.out.println("hello"); break; case "world": System.out.println("world"); break; default: break; } } }
反编译后:
public class switchDemoString { public switchDemoString() { } public static void main(String args[]) { String str = "world"; String s; switch((s = str).hashCode()) { default: break; case 99162322: if(s.equals("hello")) System.out.println("hello"); break; case 113318802: if(s.equals("world")) System.out.println("world"); break; } } }
仔细看下可以发现,进行switch的实际是哈希值,然后通过使用equals方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞
2.泛型
对于Java虚拟机来说,他根本不认识Map<String, String> map这样的语法。需要在编译阶段通过类型擦除的方式进行解语法糖。
类型擦除的主要过程如下:
-
1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
-
2.移除所有的类型参数。
Map<String, String> map = new HashMap<String, String>(); map.put("name", "hollis"); map.put("wechat", "Hollis"); map.put("blog", "www.hollischuang.com");
解语法糖之后
Map map = new HashMap(); map.put("name", "hollis"); map.put("wechat", "Hollis"); map.put("blog", "www.hollischuang.com");
虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的Class类对象。
3.自动装箱与拆箱
自动装箱的代码:
public static void main(String[] args) { int i = 10; Integer n = i; }
反编译后代码如下:
public static void main(String args[]) { int i = 10; Integer n = Integer.valueOf(i); }
再来看个自动拆箱的代码:
public static void main(String[] args) { Integer i = 10; int n = i; }
反编译后代码如下:
public static void main(String args[]) { Integer i = Integer.valueOf(10); int n = i.intValue(); }
装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的。
4.可变长参数
看下以下可变参数代码,其中print方法接收可变参数:
public static void main(String[] args) { print("Holis", "公众号:Hollis", "博客:www.hollischuang.com", "QQ:907607222"); } public static void print(String... strs) { for (int i = 0; i < strs.length; i++) { System.out.println(strs[i]); } }
反编译后代码:
public static void main(String args[]) { print(new String[] { "Holis", "\u516C\u4F17\u53F7:Hollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com", "QQ\uFF1A907607222" }); } // transient 不能修饰方法,这里应该是反编译错误了? public static transient void print(String strs[]) { for(int i = 0; i < strs.length; i++) System.out.println(strs[i]); }
从反编译后代码可以看出,可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。
5.try-with-resource
剩下还有诸多语法糖,可以参考:什么是语法糖?