Java笔试题整理

以下对找到的Java笔试题和面试题整理

1:在多线程环境中使用HashMap会有什么问题?在什么情况下使用get()方法会产生无限循环?

HashMap本身没有什么问题,有没有问题取决于你是如何使用它的。比如,你在一个线程里初始化了一个HashMap然后在多个其他线程里对其进行读取,这肯定没有任何问题。有个例子就是使用HashMap来存储系统配置项。当有多于一个线程对HashMap进行修改操作的时候才会真正产生问题,比如增加、删除、更新键值对的时候。因为put()操作可以造成重新分配存储大小(re-sizeing)的动作,因此有可能造成无限循环的发生,所以这时需要使用Hashtable或者ConcurrentHashMap,而后者更优。

2:不重写Bean的hashCode()方法是否会对性能带来影响?

这个问题非常好,每个人可能都会有自己的体会。按照我掌握的知识来说,如果一个计算hash的方法写得不好,直接的影响是,当向HashMap中添加元素的时候会更频繁地造成冲突,因此最终增加了耗时。但是自从Java 8开始,这种影响不再像前几个版本那样显著了,因为当冲突的发生超出了一定的限度之后,链表类的实现将会被替换成二叉树(binary tree)实现,这时你仍可以得到O(logN)的开销,优于链表类的O(n)。

3:对于一个不可修改的类,它的每个对象是不是都必须声明成final的?

不尽然,因为你可以通过将成员声明成非final且private,并且不要在除了构造函数的其他地方来修改它。不要为它们提供setter方法,同时不会通过任何函数泄露出对此成员的引用。需要记住的是,把对象声明成final仅仅保证了它不会被重新赋上另外一个值,你仍然可以通过此引用来修改引用对象的属性。这一点是关键,面试官通常喜欢听到你强调这一点。

4:String的substring()方法内部是如何实现的?

又一个Java面试的好问题,你应该答出“substring方法通过原字符串创建了一个新的对象”,否则你的回答肯定是不能令人满意的。这个问题也经常被拿来测试应聘者对于substring()可能带来的内存泄漏风险是否有所了解。直到Java 1.7版本之前,substring会保存一份原字符串的字符数组的引用,这意味着,如果你从1GB大小的字符串里截取了5个字符,而这5个字符也会阻止那1GB内存被回收,因为这个引用是强引用。

到了Java 1.7,这个问题被解决了,原字符串的字符数组已经不再被引用,但是这个改变也使得substring()创建字符串的操作更加耗时,以前的开销是O(1),现在最坏情况是O(n)。

5:你在写存储过程或者在Java里调用存储过程的时候如何来处理错误情况?

这是个很棘手的Java面试题,答案也并不固定。我的答案是,写存储过程的时候一旦有操作失败,则一定要返回错误码。但是在调用存储过程的时候出错的话捕捉SQLException却是唯一能做的。

6:Executor.submit()和Executor.execute()这两个方法有什么区别?

答案是:前者返回一个Future对象,可以通过这个对象来获得工作线程执行的结果。

当我们考察异常处理的时候,又会发现另外一个不同。当你使用execute提交的任务抛出异常时,此异常将会交由未捕捉异常处理过程来处理(uncaught exception handler),当你没有显式指定一个异常处理器的话,默认情况下仅仅会通过System.err打印出错误堆栈。当你用submit来提交一个任务的时候,这个任务一旦抛出异常(无论是否是运行时异常),那这个异常是任务返回对象的一部分。对这样一种情形,当你调用Future.get()方法的时候,这个方法会重新抛出这个异常,并且会使用ExecutionException进行包装。

7:什么是单例模式?创建单例对象的时候是将整个方法都标记为,synchronized好还是仅仅把创建的的语句标记为synchronized好?

在Java中,单例类是指那些在整个Java程序中只存在一份实例的类,例如java.lang.Runtime就是一个单例类。在Java 4版本及以前创建单例会有些麻烦,但是自从Java 5引入了Enum类型之后,事情就变得简单了。可以去看看如何使用Enum来创建单例类的文章,同时再看看问题五来看看如何在创建单例类的时候进行双重检查。

8:能否写一段用Java 4或5来遍历一个HashMap的代码?

事实上,用Java可以有四种方式来遍历任何一个Map,一种是使用keySet()方法获取所有的键,然后遍历这些键,再依次通过get()方法来获取对应的值。第二种方法可以使用entrySet()来获取键值对的集合,然后使用for each语句来遍历这个集合,遍历的时候获得的每个键值对已经包含了键和值。这种算是一种更优的方式,因为每轮遍历的时候同时获得了key和value,无需再调用get()方法,get()方法在那种如果bucket位置有一个巨大的链表的时候的性能开销是O(n)。第三种方法是获取entrySet之后用iterator依次获取每个键值对。第四种方法是获得key set之后用iterator依次获取每个key,然后再根据key来调用get方法。

9:你在什么时候会重写hashCode()和equals()方法?

当你需要根据业务逻辑来进行相等性判断、而不是根据对象相等性来判断的时候你就需要重写这两个函数了。例如,两个Employee对象相等的依据是它们拥有相同的emp_id,尽管它们有可能是两个不同的Object对象,并且分别在不同的地方被创建。同时,如果你准备把它们当作HashMap中的key来使用的话,你也必须重写这两个方法。现在,作为Java中equals-hashcode的一个约定,当你重写equals的时候必须也重写hashcode,否则你会打破诸如Set, Map等集合赖以正常工作的约定。你可以看看我的另外一篇博文来理解这两个方法之间的微妙区别与联系。

10:如果不重写hashCode方法会有什么问题?

如果不重写equals方法的话,equals和hashCode之间的约定就会被打破:当通过equals方法返回相等的两个对象,他们的hashCode也必须一样。如果不重写hashCode方法的话,即使是使用equals方法返回值为true的两个对象,当它们插入同一个map的时候,因为hashCode返回不同所以仍然会被插入到两个不同的位置。这样就打破了HashMap的本来目的,因为Map本身不允许存进去两个key相同的值。当使用put方法插入一个的时候,HashMap会先计算对象的hashcode,然后根据它来找到存储位置(bucket),然后遍历此存储位置上所有的Map.Entry对象来查看是否与待插入对象相同。如果没有提供hashCode的话,这些就都做不到了。

11:能否写一个单例模式,并且保证实例的唯一性

这算是Java一个比较核心的问题了,面试官期望你能知道在写单例模式时应该对实例的初始化与否进行双重检查。记住对实例的声明使用Volatile关键字,以保证单例模式是线程安全的。下面是一段示例,展示了如何用一种线程安全的方式实现了单例模式:

public class Singleton {



    private static volatile Singleton _instance;



    /**

     * Double checked locking code on Singleton

     * @return Singelton instance

     */

    public static Singleton getInstance() {

        if (_instance == null) {

            synchronized (Singleton.class) {

                if (_instance == null) {

                    _instance = new Singleton();

                }

            }

        }

        return _instance;

    }



}

 

12:我们要同步整个getInstance()方法,还是只同步getInstance()方法中的关键部分

答案是:仅仅同步关键部分(Critical Section)。这是因为,如果我们同步整个方法的话,每次有线程调用getInstance()方法的时候都会等待其他线程调用完成才行,即使在此方法中并没有执行对象的创建操作。换句话说,我们只需要同步那些创建对象的代码,而创建对象的代码只会执行一次。一旦对象创建完成之后,根本没有必要再对方法进行同步保护了。事实上,从性能上来说,对方法进行同步保护这种编码方法非常要命,因为它会使性能降低10到20倍

13:HashMap,在调用get()方法的时候equals()和hashCode()方法都起了什么样的作用

这个问题算是对问题9的补充,应聘者应该知道的是,一旦你提到了hashCode()方法,人们很可能要问HashMap是如何使用这个函数的。当你向HashMap插入一个key的时候,首先,这个对象的hashCode()方法会被调用,调用结果用来计算将要存储的位置(bucket)。

因为某个位置上可能以链表的方式已经包含了多个Map.Entry对象,所以HashMap会使用equals()方法来将此对象与所有这些Map.Entry所包含的key进行对比,以确定此key对象是否已经存在。

14:在Java中如何避免死锁

你可以通过打破互相等待的局面来避免死锁。为了达到这一点,你需要在代码中合理地安排获取和释放锁的顺序。如果获得锁的顺序是固定的,并且获得的顺序和释放的顺序刚好相反的话,就不会产生出现死锁的条件了。

15:创建字符串对象的时候,使用字面值和使用new String()构造器这两种方式有什么不同?

当我们使用new String构造器来创建字符串的时候,字符串的值会在堆中创建,而不会加入JVM的字符串池中。相反,使用字面值创建的String对象会被放入堆的PermGen段中。例如:

String str=new String(“Test”);

这句代码创建的对象str不会放入字符串池中,我们需要显式调用String.intern()方法来将它放入字符串池中。仅仅当你使用字面值创建字符串时,Java才会自动将它放入字符串池中,比如:String s=”Test”。顺便提一下,这里有个容易被忽视的地方,当我们将参数“Test”传入构造器的时候,这个参数是个字面值,因此它也会在字符串池中保存另外一份。想了解更多关于字面值字符串和字符串对象之间的差别,请看这篇文章。

下图很好地解释了这种差异。

Java笔试题整理_第1张图片

16、什么是不可修改对象(Immutable Object?)你能偶写一个例子?

不可修改对象是那些一旦被创建就不能修改的对象。对这种对象的任何改动的后果都是会创建一个新的对象,而不是在原对象本身做修改。例如Java中的String类就是不可修改的。大多数这样的类通常都是final类型的,因为这样可以避免自己被继承继而被覆盖方法,在覆盖的方法里,不可修改的特性难以得到保证了。你通常也可以通过将类的成员设置成private但是非final的来获得同样的效果。

另外,你同样要保证你的类不要通过任何方法暴露成员,特别是那些可修改类型的成员。同样地,当你的方法接收客户类型传入的可修改对象的话,你应该使用一个复制的对象来防止客户代码来修改这个刚传入的可修改类。比如,传入java.util.Date对象的话,你应该自己使用clone()方法来获得一个副本。

当你通过类函数返回一个可修改对象的时候,你也要采取类似的防护措施,返回一个类成功的副本,防止客户代码通过此引用修改了成员对象的属性。千万不要直接把你的可修改成员直接返回给客户代码

17、如何在不使用任何分析工具的情况下用最简单的方式计算某个方法的执行所花费的时间

 

在执行此方法之前和之后获取一个系统时间,取这两个时间的差值,即可得到此方法所花费的时间。

需要注意的是,如果执行此方法花费的时间非常短,那么得到的时间值有可能是0ms。这时你可以在一个计算量比较大的方法上试一下效果。

 

long start=System.currentTimeMillis();
method();
long end=System.currentTimeMillis();
System.out.println("Time taken for execution is "+(end-start));

System.currentTimeMillis()和System.nanoTime()。问题就在于,什么情况下该用哪个。从本质上来讲,他们的作用都是一样的,但有以下几点不同:

 

  • System.currentTimeMillis()的精度在千分之一秒到千分之15秒之间(取决于系统)而System.nanoTime()则能到纳秒级。
  • System.currentTimeMillis读操作耗时在数个CPU时钟左右。而System.nanoTime()则需要上百个。
  • System.currentTimeMillis对应的是绝对时间(1970年1 月1日所经历的毫秒数),而System.nanoTime()则不与任何时间点相关。
  • Float还是double

在对精度要求高的场景下,double类型相对float要更流行一些,理由如下:

大多数处理器在处理float和double上所需的时间都是差不多的。而计算时间一样的前提下,double类型却能提供更高的精度。

 

18、当你要把某个类作为HashMap的key使用的话,你需要重写这个类的哪两个方法?

为了使类可以在HashMap或Hashtable中作为key使用,必须要实现这个类自己的equals()和hashCode()方法

19、什么是可变参数

可变参数允许调用参数数量不同的方法。

//int(type) followed ... (three dot's) is syntax of a variable argument.
public int sum(int... numbers) {
//inside the method a variable argument is similar to an array.
//number can be treated as if it is declared as int[] numbers;
int sum = 0;
for (int number: numbers) {
sum += number;
}
return sum;
}

public static void main(String[] args) {
VariableArgumentExamples example = new VariableArgumentExamples();
//3 Arguments
System.out.println(example.sum(1, 4, 5));//10
//4 Arguments
System.out.println(example.sum(1, 4, 5, 20));//30
//0 Arguments
System.out.println(example.sum());//0
}

20、断言的用途

断言是在Java1.4中引入的。它能让你验证假设。如果断言失败(即返回false),就会抛出AssertionError(如果启用断言)

 

public class assert1 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub\
		int a=1;
		int b=2;
		assert(a>b) : "错误,"+a+"不大于"+b;
		
	}

}

 

Java笔试题整理_第2张图片

当断言中的表达式不成立的时候就会跑出异常,当然异常的具体描述信息需要自己添加,断言只是抛出异常。

要想断言启用,在运行时需要加入运行参数

简单来说:就是设置一下jvm的参数,参数是-enableassertions或者-ea

Java笔试题整理_第3张图片

断言不应该用于验证输入数据到一个public方法或命令行参数。IllegalArgumentException会是一个更好的选择。在public方法中,只用断言来检查它们根本不应该发生的情况。

21、什么是来及回收

垃圾回收是Java中自动内存管理的另一种叫法。垃圾回收的目的是为了程序保持尽可能多的可用堆(heap)。 JVM会删除堆上不再需要从堆引用的对象。

void method(){
Calendar calendar = new GregorianCalendar(2000,10,30);
System.out.println(calendar);
}

通过函数第一行代码中参考变量calendar,在堆上创建了GregorianCalendar类的一个对象。函数结束执行后,引用变量calendar不再有效。因此,在方法中没有创建引用到对象。JVM认识到这一点,会从堆中删除对象。这就是所谓的垃圾回收。

垃圾回收在JVM突发奇想和心血来潮时运行(没有那么糟糕)。运行垃圾收集的可能情况是:

  • 堆可用内存不足
  • CPU空闲、

用编程的方式,我们可以要求(记住这只是一个请求——不是一个命令)JVM通过调用System.gc()方法来运行垃圾回收。

当内存已满,且堆上没有对象可用于垃圾回收时,JVM可能会抛出OutOfMemoryException。

对象在被垃圾回收从堆上删除之前,会运行finalize()方法。我们建议不要用finalize()方法写任何代码。

22、什么是初始化数据块

初始化数据块,当创建对象或加载类时运行的代码

有两种类型的初始化数据块:

静态初始化器:加载类时运行的代码

实力初始化器:创建新对象时运行的代码

static{ 和 }之间的代码被称为静态初始化器。它只有在第一次加载类时运行。只有静态变量才可以在静态初始化器中进行访问。虽然创建了三个实例,但静态初始化器只运行一次。

public class InitializerExamples {
    static int count;
    int i;

    static{
        //This is a static initializers. Run only when Class is first loaded.
        //Only static variables can be accessed
        System.out.println("Static Initializer");
        //i = 6;//COMPILER ERROR
        System.out.println("Count when Static Initializer is run is " + count);
    }

    public static void main(String[] args) {
        InitializerExamples example = new InitializerExamples();
        InitializerExamples example2 = new InitializerExamples();
        InitializerExamples example3 = new InitializerExamples();
    }
}

 

 Java笔试题整理_第4张图片

实例初始化块:每次创建类的实例时,实例初始化器中的代码都会运行。

public class InitializerExamples {
	static int count;
	int i;
	{
		// This is an instance initializers. Run every time an object is
		// created.
		// static and instance variables can be accessed
		System.out.println("Instance Initializer");
		i = 6;
		count = count + 1;
		System.out.println("Count when Instance Initializer is run is " + count);
	}

	public static void main(String[] args) {
		InitializerExamples example = new InitializerExamples();
		InitializerExamples example1 = new InitializerExamples();
		InitializerExamples example2 = new InitializerExamples();
	}
}

Java笔试题整理_第5张图片

 

 

 

 

 

你可能感兴趣的:(Java知识梳理)