Java基础常见面试题——集合与杂篇
1.java容器类(集合类)?
集合总共有两大接口:Collection和Map
- Collection是元素集合,Map是键值对集合;
- ①.Collection接口是List和Set接口的父接口;
(a). List是有序可重复元素集合,Set是无序不可重复元素集合;其中接口List的实现类有ArrayList和LinkedList,ArrayList采用数组存放元素, LinkedList采用的则是链表;
(b). 接口set的实现类有HashSet、TreeSet,其中hashSet就是hashMap,treeSet默认升序; - ② HashMap 、HashTable 、TreeMap都是实现了Map接口的类,并且HashTable是线程安全的,但是HashMap性能更好。当元素的顺序很重要时选用TreeMap,当元素不必以特定的顺序进行存储时,使用HashMap。Hashtable的使用不被推荐,因为HashMap提供了所有类似的功能,并且速度更快。当你需要在多线程环境下使用时,HashMap也可以转换为线程同步的。
此图来源于:https://img-blog.csdn.net/20160124221843905
2.什么是迭代器(Iterator)?
迭代器是一种通用的collection集合元素的获取方式,它可以忽略集合中元素对象的不同,完成对集合的遍历。迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。迭代器通常被称为“轻量级”对象,因为创建它的代价小。
Java中的Iterator功能比较简单,并且只能单向移动:
(1) 使用方法iterator()要求容器返回一个Iterator。注意:iterator()方法是java.lang.Iterable接口的方法,被Collection继承。
(2) 使用hasNext()检查序列中是否还有元素。
(3) 使用next()获得序列中的下一个元素。
(4) 使用remove()将迭代器新返回的元素删除。
Iterator只能正向遍历集合,适用于获取移除元素。ListIterator继承自Iterator,专门针对List,可以从两个方向来遍历List,同时支持元素的修改(插入和删除)。
3.快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?
一、快速失败
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的结构进行了修改(增加、删除),则会抛出Concurrent Modification Exception(并发修改异常)。
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果结构发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。
二、安全失败
采用安全失败机制的集合容器,在遍历时不是直接在集合内容*问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
4.Servlet是线程安全的么?
Servlet 默认是单例模式,在web 容器中只创建一个实例,所以多个线程同时访问servlet的时候,Servlet是线程不安全的。 要解释为什么Servlet为什么不是线程安全的,需要了解Servlet容器(即Tomcat)使如何响应HTTP请求的。
当Tomcat接收到Client的HTTP请求时,Tomcat从线程池中取出一个线程,之后找到该请求对应的Servlet对象并进行初始化,之后调用service()方法。要注意的是每一个Servlet对象再Tomcat容器中只有一个实例对象,即是单例模式。如果多个HTTP请求请求的是同一个Servlet,那么着两个HTTP请求对应的线程将并发调用Servlet的service()方法。
上图中的Thread1和Thread2调用了同一个Servlet1,所以此时如果Servlet1中定义了实例变量或静态变量,那么可能会发生线程安全问题(因为所有的线程都可能使用这些变量)。多线程并不共享局部变量,所以要尽可能地在servlet中使用局部变量。
5.理解Java异常
Java把异常当作对象来处理(面向对象),并定义一个基类java.lang.Throwable作为所有异常的超类。在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error和异常Exception。
- Error和Exception的区别:Error通常是灾难性的致命的错误,是程序无法控制和处理的,当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程;Exception通常情况下是可以被程序处理的,并且在程序中应该尽可能的去处理这些异常。
- Throwable分成了两个不同的分支,一个分支是Error,它表示不希望被程序捕获或者是程序无法处理的错误。另一个分支是Exception,它表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常。其中异常类Exception又分为运行时异常(例如空指针异常)和非运行时异常(例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略)。
Java异常处理涉及到五个关键字,分别是:try、catch、finally、throw、throws。
①try–用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
②catch–用于捕获异常。catch用来捕获try语句块中发生的异常。
③finally–finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有在finally块执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
④throw --用在语句中,抛出异常。
⑤throws–用在方法中,用于声明该方法可能抛出的异常。
throw与throws的区别:
1.throws出现在方法函数头;而throw出现在函数体。
2.throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常对象。
3.两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
编程建议:
①在写程序时,对可能会出现异常的部分通常要用try{}catch{}去捕捉它并对它进行处理;
②用try{…}catch{…}捕捉了异常之后一定要对在catch{…}中对其进行处理,那怕是最简单的一句输出语句,或栈输入e.printStackTrace();
③如果是捕捉IO输入输出流中的异常,一定要在try{…}catch{…}后加finally{…}把输入输出流关闭;
④如果在函数体内用throw抛出了某种异常,最好要在函数名中加throws抛异常声明,然后交给调用它的上层函数进行处理。
6.说一下Java里面你最感兴趣的一个部分?
…
7.Java NIO你了解么?讲一讲你最熟悉的部分?
NIO即New IO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
https://mp.weixin.qq.com/s/fwkKymPOBJODo6sFHYMUHA
(1)面向流与面向缓冲
Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
(2)阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
(3)选择器(Selectors)
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
8.transient关键字?
我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。
然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。
总之,java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
9.循环结构中break、continue、return和exit的区别
1. break
break语句的使用场合主要是switch语句和循环结构。在循环结构中使用break语句,如果执行了break语句,那么就退出循环,接着执行循环结构下面的第一条语句。如果在多重嵌套循环中使用break语句,当执行break语句的时候,退出的是它所在的循环结构,对外层循环没有任何影响。如果循环结构里有switch语句,并且在switch语句中使用了break语句,当执行switch语句中的break语句时,仅退出switch语句,不会退出外面的循环结构。通过图,读者可以很直观地了解break语句的使用。
2. continue
continue语句是这四种结束循环的方式中最特殊的,因为它并没有真的退出循环,而是只结束本次循环体的执行,所以在使用continue的时候要注意这一点。图为各种循环结构中continue语句的使用。
在for循环中,首先执行表达式1(注意表达式1在整个循环中仅执行一次),接着执行表达式2,如果满足条件,那么执行循环体,如果在循环体中执行了continue语句,那么就跳转到表达式3处执行,接下进行下一次循环,执行表达式2,看是否满足条件;在while循环中,如果执行了continue语句,那么就直接跳转到表达式处,开始下一次的循环判断;在do while循环体中如果执行了continue语句,那么就跳转到表达式处进行下一次的循环判断,这一点前面已经验证过了。
3. return语句
如果在程序中遇到return语句,那么代码就退出该函数的执行,返回到函数的调用处,如果是main()函数,那么结束整个程序的运行。
4. exit()函数
exit()函数与return语句的最大区别在于,调用exit()函数将会结束当前进程,同时删除子进程所占用的内存空间,把返回信息传给父进程。当exit()中的参数为0时,表示正常退出,其他返回值表示非正常退出,执行exit()函数意味着进程结束;而return仅表示调用堆栈的返回,其作用是返回函数值,并且退出当前执行的函数体,返回到函数的调用处,在main()函数中, return n和exit(n)是等价的。
10.大数问题与进制转换
BigInteger 和 BigDecimal 是在java.math包中已有的类,前者表示整数,后者表示浮点数。
package com.liuwen.练习.大数;
import java.math.*;
/**
* @description: Good good study,day day up!
* @author: Liu Wen
* @create: 2020-02-26 13:05
**/
public class Main{
public static void main(String[] args) {
//大数加减乘除
int a = 156, b = 55, c = 1652;BigInteger x, y, z, ans;
x = BigInteger.valueOf(a);y = BigInteger.valueOf(b);z = BigInteger.valueOf(c);
double d = 166.7,e = 55.5;BigDecimal num1,num2,res;
num1 = BigDecimal.valueOf(d);num2 = BigDecimal.valueOf(e);
ans = x.add(y);
System.out.println("a+b= "+ans); //a+b= 211
ans = x.subtract(y);
System.out.println("a-b= "+ans); //a-b= 101
ans = x.multiply(y);
System.out.println("a*b= "+ans); //a*b= 8580
ans = z.divide(y);
System.out.println("c/b= "+ans); //c/b= 30
ans = z.remainder(y);
System.out.println("c%b= "+ans); //c%b= 2
res = num1.add(num2);
System.out.println("num1+num2= "+res); //num1+num2= 222.2
res = num1.subtract(num2);
System.out.println("num1-num2= "+res); //num1-num2= 111.2
res = num1.multiply(num2);
System.out.println("num1*num2= "+res); //num1*num2= 9251.85
res = num1.divide(num1);
System.out.println("num1/num1= "+res); //num1/num1= 1
res = num1.remainder(num2);
System.out.println("num1%num2= "+res); //num1%num2= 0.2
//进制转换
String result1 = Integer.toHexString(15); //十进制转成十六进制:f
String result2 = Integer.toOctalString(15) ; //十进制转成八进制:17
String result3 = Integer.toBinaryString(15); //十进制转成二进制:1111
String result4 = Integer.valueOf("10",16).toString(); //十六进制转成十进制:16
String result5 =Integer.valueOf("17",8).toString(); //八进制转成十进制:15
String result6 = Integer.valueOf("0101",2).toString(); //二进制转十进制:5
System.out.println("十进制转成十六进制:"+result1);
System.out.println("十进制转成八进制:"+result2);
System.out.println("十进制转成二进制:"+result3);
System.out.println("十六进制转成十进制:"+result4);
System.out.println("八进制转成十进制:"+result5);
System.out.println("二进制转十进制:"+result6);
}
}