您的位置:首页 > 职场人生

JAVA面试问题—基础篇(上)

2017-07-12 12:28 155 查看

JAVA面试问题—基础篇(上)

1.JAVA基本数据类型及其大小

Long(8),Int(4),Short(2),Double(8),Float(4),Char(2),Boolean(1),void

2.equals()与“==”的区别

equals()方法比较的是对象引用是否相等

“==”比较的是对象的地址是否相等

3.Object类有哪些公用方法

wait()、notify()、notifyAll():这三个方法用于JAVA并发编程,wait()方法暂停对象中的线程,notify和notifyAll方法则用于唤醒他们,值得注意的是 wait方法会放弃所持有的锁,sleep(thread类)方法则会保留锁。

hashCode()、equals():这两个方法用于对象的散列和比较,hashCode方法总在equals方法中使用,可以提高对象的比较效率(对象相等则hashcode相等,hashcode相等不代表对象相等),一般使用不可变对象左右hashCode的KEY值。

equals()方法仅比较对象的引用,若要比较对象的具体值是否相等,则需要在自己的对象中重写equals方法。

当hashMap,hashTable或者hashSet(value本身就是key)中作为key的时候,如过重写equal方法,则要求重写hashCode()方法。

Clone():该方法用于复制一个对象,需要实现Cloneable接口,复制完成后对象实例相等(即内存数据),但引用不相等。如果一个对象中有一个引用作为变量,那么该应用也直接复制,这样就导致两个两个内存区域的引用指向同一个对象,因此这叫浅引用。

getClass():该方法用于获取一个Class对象,属于动态加载的范畴,主要用于反射(Class.forName()功能类似)。类名.class也可以完成相似的功能,但其属于编译时期的静态加载。

toString():该方法用于输出类字符串。

finalize():该方法与JVM垃圾回收相关。JVM在进行垃圾回收时会查看待回收对象(可达性分析后没有与GC roots直接或间接相连)的该方法是否被覆盖或被调用过。若没有,则进行一次标记。被覆盖该方法的对象会自动调用该方法,此时在该方法中可以重新与GC roots建立连接(赋值给类中的静态变量等操作)。第二次标记时如果该对象仍没有建立连接,则回收该对象。

4. ArrayList、LinkedList、Vector的区别。

ArrayList与Vector,两者都是基于数组进行实现的,且都实现了List接口。他们都拥有数组的特性,即随机查快,增删慢(除末尾)。此外Vector的方法中加入了synchronized关键字,所以Vector是线程安全的,但效率略低于ArrayList。两者也都有扩容问题(扩容因子,扩容倍数(2/1.5),数据拷贝)。适合稳定数据。

LinkedList的实现基于双向循环链表,继承List和Queue两个接口。可以作为队列使用,且不存在扩容问题,但也有链表的特性,增删快、随机查找慢(首末节点除外)。适合快速增长数据。

5.String、StringBuffer与StringBuilder的区别。

String对象不可变,底层char[]使用final关键字修饰,使用new String对象作为返回值。

StringBuffer和StringBuilder,都是可变的。首先对他们进行操作后返回的是this对象,即本身。其次,两者都继承abstractStringBuilder类,该类的底层char[]没有使用final关键字。且StringBuffer类中使用了synchronsized关键字修饰方法,所以他是线程安全的。

在字符串进行大量连接操作时,要尽量避免使用String进行操作,减少对象创建销毁的开销。

6.Map、Set、List、Queue、Stack的特点与用法。

Map:以键值对存储数据,可且仅可存储一个null作为KEY.

Set:继承collection接口,数据存入没有顺序。数据不可重复,可以存储null,使用HashMap中的Key值存放Set的Value。

List:继承collection接口,可存null,有顺序。

Stack:先入后出,继承Vector,使用数组实现,可用LinkedList实现,线程安全。

Queue:先入先出,ListedList继承该类,可以使用LinkedList进行实现。

7.HashMap 和 HashTable 的区别

HashMap,线程不安全,允许key值为NULL,继承AbstractMap类,实现Map接口。

HashTable,线程安全,不允许Key值为NULL(有空指针异常判断),继承Dictionary类,实现Map接口。

可以使用Collections.synchronizedMap(HashMap),来实现线程安全,但是是对整个对象加锁,效率不高。

8.HashMap和ConcurrentHashMap的区别

HashMap采用数组+链表的形式实现。HashMap中有一个Entry[]数组,存入数据时会根据HashCode计算决定将数据存储Entry数据的哪个位置,如果计算出的位置已有数据,则使用链表将数据链接在原数据之后。当数据持续插入时,原有的Entry数组容量出现紧缺,就可能产生rehash,创建大小为原数组大小两倍的数组,在将数据复制过去。所以,当我们可以预估数据量的时候,可以在初始化时指定Entry数组的大小,以避免移动数据的开销。(附:hash冲突时,保证每次扩容2的N次方,使其恰好预估值大于,其次注意负载因子,该因子越大利用率越高越易冲突,反之亦成立。)

ConcurrentHashMap使用二次哈希的方式实现分段锁,可以将ConcurrentHashMap看作由许多HashMap组成的,它对每一个子HashMap分别加锁,将他们相互隔离,即使有一个Map被加锁也不会影响其他Map的工作效率。Collections.synchronized则是对整个Map加锁。

工作流程为,1.进行第一次HASH确定子Map(segment[]数组中有众多MAP)2.进行二次Hash确定子Map中(hashEntry[])应该存储的位置。

9.TreeMap、HashMap、LindedHashMap的区别

TreeMap采用红黑树实现,因此对树的查找的复杂度时O(log2n).而LinkedHashMap采用哈希数组加双向循环链表的形式实现,查找的复杂度是O(1).即在HashMap的基础上维护一个双向循环链表,该链表可以通过设置accessOrder来设置是否启用LRU,默认为插入顺序。其实现方式为,每插入一个节点先与之前的节点建立一个双向循环链表,再按HashMap的方式插入数据,这样将遍历与查找分离开来,从而提高效率。

10.Collection包结构及其与Collections的区别

Collection被称为类集,包含四个接口,List、Set、Queue、BeanContext。使用类集对各个容器实现了标准化,易于扩展我们自己的类。

其中List(abstractList类,Vector类,LinkedList类,ArrayList类(同时继承abstractList),copyOnwriteArrayLIst类)、Set(AbstractSet实现,其他标准继承abstractSet,实现由hashSet类,copyonwriteArraySet类,TreeSet类(跟hashSet类似,也是用TreeMap实现的),ConcurrentSkipArraySet)

Collections是集合类的一个工具类,提供一系列静态方法对集合进行排序、搜索、线程安全等操作(Collections.sort(),Collections.Copy(),Colections.sysnchronizedList()Collections.sysnchronizedMap()等)。

11.try catch finally,try里有return,finally还执行么?

1、不管有木有出现异常,finally块中代码都会执行;

2、当try和catch中有return时,finally仍然会执行;

3、finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally执行前确定的;

4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。

12.
4000
Exception与Error包结构。

Throwable是所有异常和错误的父类,Exception类和Error类都继承该类,其中Exception类又分为运行时异常(RuntimeException)和非运行时异常。RuntimeException不由Try/Catch抛出,而是由系统在运行时自动抛出,如果使用Try/Catch则不会显示。

Error:一般是程序无法处理如错误,如OutOfMemoryError(List对象中添加大量对象),StackOverFlowError(虚拟机栈、本地方法栈中,String.Inter()动态加入常量),ThreadDeath等。直接触发线程中止。

运行时异常:NullPointerException、IndexOfBoundExecption等。

非运行时异常:IoExecption、SqlException等。

13.面向对象三大特征

封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。封装是面向对象的特征之一,是对象和类概念的主要特性。简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。

继承:指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。

实现继承与接口继承。实现继承是指直接使用基类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力;

多态:指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。重写和重载都是多态的一种表现形式。

多态的实现依赖于多态机制,即动态绑定:也就是在编译的时候编译器只通过父类引用只知道调用方法的签名,无法知道调用哪一个方法体,要确定这个调用的方法体,只有通过new 创建一个对象以后,并将该方法对应的栈帧加入虚拟机栈中才能知道实际的方法体,也就是方法体的后期绑定,因此就不能像使用final修饰方法一样,编译器自己可以优化方法。

14.Override和Overload的含义

override是覆盖,覆盖是子类和父类之间多态性的表象形式

overload是重载。重载的原则是方法的签名(参数数量、参数顺序、参数类型)不同,方法签名不包括返回值哦,重载是一个类中多态的表现形式。

15.Interface与abstract类的区别

1.相同点

A. 两者都是抽象类,都不能实例化。

B. interface实现类及abstract class的子类都必须要实现已经声明的抽象方法。(abstract类,是没有足够具体信息描绘出的类)

2. 不同点

A. interface需要实现,要用implements,而abstract class需要继承,要用extends。

B. 一个类可以实现多个interface,但一个类只能继承一个abstract class。

C. interface强调特定功能的实现“like a”,而abstract class强调所属关系“is a”。

D. 尽管interface实现类及abstrct class的子类都必须要实现相应的抽象方法,但实现的形式不同。interface中的每一个方法都是抽象方法,都只是声明的 (declaration, 没有方法体),实现类必须要实现。而abstract class的子类可以有选择地实现。

E.abstract类可以有自己的私有变量,interface中的属性必须是static final的。

16.Static class 与non static class的区别

静态类只有内部静态类一种(很少使用,不过可以用作单元测试)。其余的都是外部类。

内部静态类不需要有指向外部类的引用。但非静态内部类需要持有对外部类的引用。

非静态内部类能够访问外部类的静态和非静态成员。静态类不能访问外部类的非静态成员。他只能访问外部类的静态成员。

一个非静态内部类不能脱离外部类实体被创建,一个非静态内部类可以访问外部类的数据和方法,因为他就在外部类里面。

17.JAVA多态的实现原理

多态即类的多种形态,具体表现为重写和重载。

多态的实现主要体现在编译阶段和运行时阶段,编译阶段主要是静态分派,即根据静态类型和方法参数来选择使用哪一个版本的方法,这就是方法的重载。

运行阶段主要是动态分派(绑定),当父类引用指向子类对象,通过父类去调用子类方法。把运行时常量池中符号引用解析为不同的直接引用。invokevirtual指令的运行时解析过程大致分为以下几个步骤:

1、找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。

2、如果在类型C中找到与常量中的描述符和简单名称相符合的方法,然后进行访问权限验证,如果验证通过则返回这个方法的直接引用,查找过程结束;如果验证不通过,则抛出java.lang.IllegalAccessError异常。

3、否则未找到,就按照继承关系从下往上依次对类型C的各个父类进行第2步的搜索和验证过程。

4、如果始终没有找到合适的方法,则跑出java.lang.AbstractMethodError异常。

由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称动态分派。

对动态绑定的优化:由于要去常量池中搜索每一个类的方法名和描述符,因此效率较低。在方法区中为每一个类维护一个虚方法表或接口方法表,将该类所有方法都维护进去(包括父类方法),在查找时,直接根据该表的方法名找到方法的入口地址,对于没有重写的方法,直接使用父类的方法地址。

18.Java实现多线程的两种方式。

实现Runnable接口,由于JAVA的单继承机制,一旦该类继承了一个父类,便不可以使用 extends Thread的方式创建线程。而JAVA可以实现多个接口,此时可以实现Runnable接口来实现多线程。

继承Thread类:本质上使用这种方式和实现Runnable接口是相同的,因为Thread类也是实现Runnable接口的。

在实际运用种,使用Runnable接口更好,不必受到单继承机制的影响。

Thread thread=new Thread(“线程1”),thread.start();

MyTread thread=new MyTread(线程1);new Thread(thread).start();

19.线程同步的方法:synchronized、lock、reentrantLock等

synchronized 是一个同步锁关键字,可以用来修饰代码块、方法、静态方法、类。修饰的不同范围决定了同步锁影响的范围。修饰代码块影响调用该代码块的对象、修饰方法影响调用该方法的对象、修饰静态方法影响调用该方法的所有对象、修饰类名影响该类所有对象。但是该关键字不会影响对象种非synchronized的其他方法。至于影响范围种所有对象和对象的区别在于多个线程中由同类生成的多个对象是否发生互斥,如果互斥则是全部对象,如果不互斥则是单个对象。

发生上述情况的原因:每个线程都拥有自己的栈,栈中保存对应方法的栈帧(返回地址、局部变量、动态链接、操作数栈等)。当创建多个对象时,每个对象都拥有自己的资源,此时每个线程中栈对象的引用分别指向各自的对象,自然不存在资源互斥的问题。但是,当多个线程的对象引用指向同一个对象时,则会出现资源互斥的情况。对于静态方法而言,静态方法存储在JVM的方法区中,方法区中的资源是共享资源,此时使用synchronized锁定静态方法就相当于锁定了一份静态资源,自然会产生互斥。

Lock锁:Lock锁存在的原因是,使用synchronized关键字进行修饰一个资源时,对该资源的占有时间过长(如读取IO或sleep()时),其他线程想要获取该资源时只能等待,效率低下。其次,对于读写可以分离的资源,synchronized无法做到读读不互斥,读写、写写互斥,他会全部一概而论。Lock接口的几个方法,lock(),tryLock()(尝试获取锁,加入时间参数后可以在规定时间内尝试获取锁),lockInterruptibly()(中断阻塞状态的线程,已经运行的则不行),unlock()(释放锁).

Lock与synchronized关键字有几处区别:首先synchronized是java内置的关键字,而Lock则是java.util.concurrent.locks包下的接口。synchronized对资源的管理比较简单,会自动释放资源,比较安全。而Lock则不会自动释放锁住的资源,因此很容易造成死锁。使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

ReentrantLock类:该类是唯一实现了lock接口的类,提供了更多的方法。拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。

ReadWriteLock接口,就是用来获取读锁和写锁的,ReentrantReadWriteLock类实现了readwriteLock接口。private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();rwl.readLock().lock();尝试获取读锁,如果一个线程占用了读锁,别的线程来申请读锁的时候,还是会给该锁,如果领个线程中是rwl.writeLock().lock()则代笔是申请写锁,此时会失败,synchronized并不具备这样的功能。

20.锁的等级:方法锁,对象锁,类锁

方法锁,对方法进行加锁。

对象锁,也就是以一个对象做为锁,也就是synchronized(obj)关键字中的对象;此时该锁只是针对一个对象的,因为对象在内存中可以有多个,也可以有一个。

类锁:以一个静态变量(也可以是 类名.class,或静态方法)作为锁对象。此时所有使用他们的对象都会受到影响。

此外。

可重入锁:只得是如果一个线程获取了一个对象锁以后,如果该线程获取该对象锁锁定的方法块,则会直接进入,而不用重新申请锁;Lock和syschronized都是可重入锁。(可重入就是进入一次,计数器+1,每出来一次,计数器-1)

可中断锁:如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。Lock就是可中断锁,而sysnchronized是不可中断的。

公平锁:公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。非公平锁则可能是随机的;有的线程永远也不能获得该锁。synchronized就是非公平锁,ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

读写锁:读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。

21.写出生产者消费者模式。

1.用synchronized关键字和wait()/notifyAll()实现

import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
*    synchronized实现的生产者和消费者
*/
public class ProducerCustomerWithSynchronized {

Executor pool = Executors.newFixedThreadPool(10);

//仓库
private List<String> storageList = new LinkedList<String>();

//仓库容量
private int MAX_SIZE = 3;

//仓库为空
private int ZERO = 0;

//生产者线程
private class Producer implements Runnable{

//生产方法,需同步
private void produce(){
synchronized (storageList) {
try {
if(storageList.size()==MAX_SIZE) {
Thread.sleep(1000);
storageList.wait();//当前线程放弃锁,处于等待状态,让其他线程执行
}
if(storageList.size()<MAX_SIZE) {
storageList.add(name);
System.out.println
}
Thread.sleep(1000);
storageList.notifyAll();//当前线程放弃锁,唤醒其他线程

}catch(InterruptedException ie) {
System.out.println("中断异常");
ie.printStackTrace();
}

}
}

@Override
public void run() {
while(true) {
produce();
}
}
}

//消费者线程
private class Customer implements Runnable{

//消费方法,需同步
private void consume() {
synchronized (storageList) {
try {
if(storageList.size()==ZERO) {
Thread.sleep(1000);
storageList.wait();//当前线程放弃锁,处于等待状态,让其他线程执行
}
if(storageList.size()!=ZERO) {
storageList.remove(0);
}
Thread.sleep(1000);
storageList.notifyAll();//当前线程放弃锁,唤醒其他线程
}catch(InterruptedException ie) {
System.out.println("中断异常");
ie.printStackTrace();
}

}
}

@Override
public void run() {
while(true) {
consume();
}
}

}

//启动生产者和消费者线程
public void start() {
for(int i=1;i<5;i++) {
//new Thread(new Producer()).start();
//new Thread(new Customer()).start();
pool.execute(new Producer());
pool.execute(new Customer());
}

}

public static void main(String[] args) {
ProducerCustomerWithSynchronized pc = new ProducerCustomerWithSynchronized();
pc.start();
}
}


2.用ReentrantLock锁和Condition变量来实现

import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
* Lock实现的生产者和消费者
*
*/
public class ProducerCustomerWithLock {

Executor pool = Executors.newFixedThreadPool(10);

//仓库
private List<String> storageList = new LinkedList<String>();

//仓库容量
private int MAX_SIZE = 3;

//仓库为空
private int ZERO = 0;

//获取锁对象
private Lock lock = new ReentrantLock();

//仓库满了,绑定生产者线程
private Condition full = lock.newCondition();

//仓库为空,绑定消费者线程
private Condition empty = lock.newCondition();

//生产者线程
private class Producer implements Runnable{

//生产方法,需同步
private void produce(){
if(lock.tryLock()) {
try {
if(storageList.size()==MAX_SIZE) {
Thread.sleep(1000);
full.await();//生产者线程加入线程等待池
}
if(storageList.size()<MAX_SIZE){
storageList.add(new Random().nextInt());
}
Thread.sleep(1000);
empty.signalAll();//唤醒消费者线程

}catch(InterruptedException ie) {
System.out.println("中断异常");
ie.printStackTrace();
}finally{
lock.unlock();
}
}
}

@Override
public void run() {
while(true) {
produce();
}
}
}

//消费者线程
private class Customer implements Runnable{

//消费方法,需同步
private void consume() {
if(lock.tryLock()) {
try {
if(storageList.size()==ZERO) {
Thread.sleep(1000);
empty.await();//消费者线程加入线程等待池
}
if(storageList.size()!=ZERO) {
storageList.remove(0);
}
Thread.sleep(1000);
full.signalAll();//唤醒生产者线程
}catch(InterruptedException ie) {
System.out.println("中断异常");
ie.printStackTrace();
}finally{
lock.unlock();
}
}
}

@Override
public void run() {
while(true) {
consume();
}
}
}

//启动生产者和消费者线程
public void start() {
for(int i=1;i<5;i++) {
//new Thread(new Producer()).start();
//new Thread(new Customer()).start();
pool.execute(new Producer());
pool.execute(new Customer());
}
}
public static void main(String[] args) {
ProducerCustomerWithLock pc = new ProducerCustomerWithLock();
pc.start();
}
}


22.ThreadLocal的设计理念与作用(线程局部变量)

设计理念:ThreadLocal中有一个静态内部类ThreadLocalMap(该map就理解为hashMap吧,只是该map中的enty实体继承了弱引用,也就是说,如果该Map中的对象太多的时候,即使我们没有主动放弃,可能垃圾回收器回收);同时在Thread类中有一ThreadLocalMap的组合,为ThreadLocal.ThreadLocalMap;当在一个线程中调用local.set(100)方法的时候,则在该线程对象的ThreadLocalMap中存入{local,100};当通过local.get()方法的时候,则先获取当前线程,通过当前线程获取ThreadLocalMap,然后,通过该local变量为key值去获取对应的value(100);

TheadLocal变量不是用来实现资源共享的,而是用来实现资源的在各个线程中的隔离。各个线程单独占有。虽然也可用局部变量,但是有些场景,如果说资源只有一个的时候,就得使用ThreadLcoal变量。比如说DAO的数据库连接,我们知道DAO是单例的,那么他的属性Connection就不是一个线程安全的变量。而我们每个线程都需要使用他,并且各自使用各自的。还有要注意是对一个对象,如ThreadLocal这种状况,则map中存的是指向person的引用。要特别小心。

23.ThreadPool用法与优势

Java通过Executors提供四种线程池,分别为:

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行(设置多长时间后该线程执行;每隔几秒执行一次)。

newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行(保证只有一个线程执行任务,与newFixedThreadPool(1)等价)。

线程池用法:Executors中有很多静态方法可以用来创建线程池;Executor是一个工厂方法类;里面可以创建很多线程;所有的ThreadPool都实现了Executor这个接口;因此只要返回Executor(该接口只有一个方法,那就是executor(Runnable thread))就可以了。ExecutorService cachedThreadPool = Executors.newCachedThreadPool();cachedThreadPool.execute(new Runnable() public void run() { System.out.println(index); } 也执行一次execute方法,就产生一个新的线程;

创建和销毁一个线程的开销并不小,因此采用线程池的方式对线程进行管理。将预先创建好的线程放入一个队列中,当需要时从线程池队列中申请一个线程使用,使用结束后将线程还给线程池。避免多次创建和销毁线程带来的开销。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 面试