面试题系列(三):Java的基础知识面试考点精华(Collection、Map、Thread……)

说明:这篇文章是在“Java知音”中看到的,感觉很不错,就拿出来分享给大家,另外这是一个系列的文章,后续可能还有新的内容出来,这里将会一直关注,有更新就会及时在博客上转载,分享给大家。同时也作为自己的一个知识储备。

金三银四人才招聘的高峰期,渴望跳槽的朋友肯定跟我一样四处找以往的面试题,但又感觉找的又不完整,在这里我将把我所见到的题目做一总结,并尽力将答案术语化、标准化。预祝大家面试顺利。

建议:术语会让你的面试更有说服力,让你感觉更踏实,建议大家多记背点术语。

简单说一下什么是跨平台

术语:操作系统指令集、屏蔽系统之间的差异

由于各种操作系统所支持的指令集不是完全一致,所以在操作系统之上加个虚拟机可以来提供统一接口,屏蔽系统之间的差异。

Java有几种基本数据类型

有八种基本数据类型。

数据类型 字节 默认值
byte 1 0
short 2 0
int 4 0
long 8 0
float 4 0.0f
double 8 0.0d
char 2 ‘\u0000’
boolean 4 false

注意:这里在boolean类型的字节上是有争议的,因为在不同情况下,对应的字节数会有变化

Java虚拟机规范书中定义:

  • boolean编译后会使用int数据类型来代替,所以大小是4字节
  • boolean数组编译后会用byte数组来代替,就是1字节

ArrayList和LinkedList区别?

  1. ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
  2. 对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
  3. 对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。

如果更深入的问你源码的话也不要怂,下面是对应的源码分析文章。

面试必会之ArrayList源码分析&手写ArrayList

面试必会之LinkedList源码分析

HashMap和HashTable、ConcurrentHashMap区别?

相同点:

  1. HashMap和Hashtable都实现了Map接口
  2. 都可以存储key-value数据

不同点:

  1. HashMap可以把null作为key或value,HashTable不可以。
  2. HashMap线程不安全,效率高。HashTable线程安全,效率低。
  3. HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。

什么是fail-fast?
就是最快的时间能把错误抛出而不是让程序执行。

如何保证线程安全又效率高?

Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。

ConcurrentHashMap将整个Map分为N个segment(类似HashTable),可以提供相同的线程安全,但是效率提升N倍,默认N为16。(Java8中对ConcurrentHashMap和HashMap的存储结构做了变动,这里是需要看的注意点,比如问你Java8和Java7中着两个集合数据结构有什么变化)

我们能否让HashMap同步?

HashMap可以通过下面的语句进行同步:
Map m = Collections.synchronizeMap(hashMap);

这种方式知道了解一下即可(作为一个你对Java基础知识了解程度的证明),但是实际是不可用的,完全可以用ConcurrentHashMap替换,因为这种方式会大大削减HashMap的性能,而且会低于ConcurrentHashMap性能,注意对比选择最优方案。

拷贝文件的工具类使用字节流还是字符流

答案:字节流

什么是字节流,什么是字符流?

  • 字节流:传递的是字节(二进制)
  • 字符流:传递的是字符

String、StringBuffer和StringBuilder区别

java中String、StringBuffer、StringBuilder是编程中经常使用的字符串类,他们之间的区别也是经常在面试中会问到的问题。现在总结一下,看看他们的不同与相同。

数据可变和不可变

  1. String底层使用一个不可变的字符数组private final char value[];所以它内容不可变。
  2. StringBufferStringBuilder都继承了AbstractStringBuilder底层使用的是可变字符数组:char[] value;

线程安全

  • StringBuilder是线程不安全的,效率较高;而StringBuffer是线程安全的,效率较低。

通过他们的append()方法来看,StringBuffer是有同步锁,而StringBuilder没有:

1
2
3
4
5
6
7
8
9
10
11
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}

相同点

StringBuilderStringBuffer有公共父类AbstractStringBuilder

最后,操作可变字符串速度:StringBuilder > StringBuffer > String,这个答案就显得不足为奇了。

讲一下Java中的集合

  • Collection下:List系(有序、元素允许重复)和Set系(无序、元素不重复)

set根据equals和hashcode判断,一个对象要存储在Set中,必须重写equals和hashCode方法

  • Map下:HashMap线程不同步;TreeMap线程同步

  • Collection系列和Map系列:Map和Collection是互为补充,两个没什么关系

线程创建方式

方法一:继承Thread类,作为线程对象存在(继承Thread对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class CreatThreadDemo1 extends Thread{
/**
* 构造方法: 继承父类方法的Thread(String name);方法
* @param name
*/
public CreatThreadDemo1(String name){
super(name);
}

@Override
public void run() {
while (!interrupted()){
System.out.println(getName()+"线程执行了...");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
CreatThreadDemo1 d1 = new CreatThreadDemo1("first");
CreatThreadDemo1 d2 = new CreatThreadDemo1("second");

d1.start();
d2.start();

d1.interrupt(); //中断第一个线程
}
}

常规方法,不多做介绍了,interrupted方法,是来判断该线程是否被中断。(终止线程不允许用stop方法,该方法不会施放占用的资源。所以我们在设计程序的时候,要按照中断线程的思维去设计,就像上面的代码一样)。

让线程等待的方法

  • Thread.sleep(200); //线程休息2ms
  • Object.wait(); //让线程进入等待,直到调用Object的notify或者notifyAll时,线程停止休眠

方法二:实现runnable接口,作为线程任务存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CreatThreadDemo2 implements Runnable {
@Override
public void run() {
while (true){
System.out.println("线程执行了...");
}
}

public static void main(String[] args) {
//将线程任务传给线程对象
Thread thread = new Thread(new CreatThreadDemo2());
//启动线程
thread.start();
}
}

Runnable 只是来修饰线程所执行的任务,它不是一个线程对象。想要启动Runnable对象,必须将它放到一个线程对象里。

方法三:匿名内部类创建线程对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class CreatThreadDemo3 extends Thread{
public static void main(String[] args) {
//创建无参线程对象
new Thread(){
@Override
public void run() {
System.out.println("线程执行了...");
}
}.start();
//创建带线程任务的线程对象
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程执行了...");
}
}).start();
//创建带线程任务并且重写run方法的线程对象
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("runnable run 线程执行了...");
}
}){
@Override
public void run() {
System.out.println("override run 线程执行了...");
}
}.start();
}

}

创建带线程任务并且重写run方法的线程对象中,为什么只运行了Thread的run方法。从Thread类的源码可以看到Thread实现了Runnable接口,而Runnable接口里有一个run方法。所以,我们最终调用的重写的方法应该是Thread类的run方法。而不是Runnable接口的run方法。

方法四:创建带返回值的线程(Callable)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class CreatThreadDemo4 implements Callable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CreatThreadDemo4 demo4 = new CreatThreadDemo4();

FutureTask<Integer> task = new FutureTask<Integer>(demo4); //FutureTask最终实现的是runnable接口

Thread thread = new Thread(task);

thread.start();

System.out.println("我可以在这里做点别的业务逻辑...因为FutureTask是提前完成任务");
//拿出线程执行的返回值
Integer result = task.get();
System.out.println("线程中运算的结果为:"+result);
}

//重写Callable接口的call方法
@Override
public Object call() throws Exception {
int result = 1;
System.out.println("业务逻辑计算中...");
Thread.sleep(3000);
return result;
}
}

Callable接口介绍:

1
2
3
4
5
6
7
8
9
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}

返回指定泛型的call方法。然后调用FutureTask对象的get方法得道call方法的返回值。

方法五:定时器Timer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CreatThreadDemo5 {

public static void main(String[] args) {
Timer timer = new Timer();

timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定时器线程执行了...");
}
},0,1000); //延迟0,周期1s

}
}

方法六:线程池创建线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CreatThreadDemo6 {
public static void main(String[] args) {
//创建一个具有10个线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
long threadpoolUseTime = System.currentTimeMillis();
for (int i = 0;i<10;i++){
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程执行了...");
}
});
}
long threadpoolUseTime1 = System.currentTimeMillis();
System.out.println("多线程用时"+(threadpoolUseTime1-threadpoolUseTime));
//销毁线程池
threadPool.shutdown();
threadpoolUseTime = System.currentTimeMillis();
}

}

方法七:利用java8新特性 stream 实现并发

lambda表达式不懂的,可以看看我的java8新特性文章:

1
2
3
4
5
6
7
8
9
10
public class CreatThreadDemo7 {
public static void main(String[] args) {
List<Integer> values = Arrays.asList(10,20,30,40);
//parallel 平行的,并行的
int result = values.parallelStream().mapToInt(p -> p*2).sum();
System.out.println(result);
//怎么证明它是并发处理呢
values.parallelStream().forEach(p-> System.out.println(p));
}
}

输出:

200
40
10
20
30

怎么证明它是并发处理呢,他们并不是按照顺序输出的 。


本文作者:Java知音(微信公众号)
原文地址:https://mp.weixin.qq.com/s/rXA_UjF52RGbT7bEykD4mA
版权归作者所有,转载请注明出处

支付宝 微信

如果文章对你有帮助,欢迎点击上方按钮打赏作者