Java 面试准备

ImportNew 网站的Java面试专题学习笔记

1. 非可变性和对象引用

String s = " Hello ";
s += " World ";
s.trim( );
System.out.println(s);

输出为“ Hello World ”,前后皆有空格。
字符串是不可变对象。
s.trim()虽然生成了一个新的字符串对象,但是却没有变量指向这个心生成的对象,s 仍然指向字符串s += " World "。
下图说明了,生成对象以及垃圾回收过程。

可用StringBuilder来构造,因为其底层使用的是字符数组,所有操作都直接在字符数组上直接操作,而且他不是一个线程安全的类,执行速度上,相比于StringBuffer要快。

这一点如果深入理解了String的Interning机制,就更好理解了。
Java程序在编译时,会将所有确定下来的,存在双引号内的字符串,都存入常量池中,该常量池是类文件(.class)的一部分。常量池中存着许多表,其中 Constant_Utf8_info 表中,记录着会被初始化为 String 对象的字符串的字面值(iteral)。 在JVM 中,相应的类被加载运行后,常量池对应的映射到 JVM 的运行时常量池(run time constant pool)中。

关于String的intern()方法

当 intern 方法被调用,如果字符串池中已经拥有一个与该 String 的字符串值相等(即 equals()调用后为 true)的 String 对象时,那么池中的那个 String 对象会被返回。否则,池中会增加这个对象,并返回当前这个 String 对象。

现代的 JVM 实现里,考虑到垃圾回收(Garbage Collection)的方便,将 heap 划分为三部分: young generation 、 tenured generation(old generation)和 permanent generation( permgen )。字符串池是为了解决字符串重复的问题,生命周期长,它存在于 permgen 中。

因此,对于

String str = new String("abc");

JVM会生成两个字符串,一个是在常量池中,另外一个是new在heap堆中。

2. equals 和 ==

Object s1 = new String("Hello");
Object s2 = new String("Hello");
 
if(s1 == s2) {
  System.out.println("s1 and s2 are ==");
}else if (s1.equals(s2)) {
  System.out.println("s1 and s2 are equals()");
}

输出结果为s1 and s2 are equals()
主要考察对equals和==的理解,==比较引用的地址是否相同,equeal比较对象中真正的值。
详细的过程可见下图

另外,如果不是用 new关键字强制创建字符串对象的话,而是采用==,那么Java会默认采用字符串池,减少对象的创建。

String对象会创建一个字符串池(a pool of string),如果当前准备新创建的字符串对象的值在这个池子中已经存在,那么就不会生成新对象,而是复用池中已有的字符串对象。flyweight 模式的精髓就是对象复用。

3. 重载(overloading)和重写(overriding)

重写发生在子类继承父类时,子类覆盖父类的方法时发生,是在运行时发生。
重载是在同一个类中,同一个方法,不同参数时发生,是在编译期发生。

在Java 5中使用注解@override,来标示方法重写,如果编译时发现没有重写,则JVM会抛出编译异常。

4. 迭代和递归

可重入方法(re-entrant method)是可以安全进入的方法,即使同一个方法正在被执行,深入到同一个线程的调用栈里面也不会影响此次执行的安全性。一个非可重入方法则不是可以安全进入的。例如,加入写文件或者向文件中写入日志的方法不是可重入方法时,有可能会毁坏那个文件。

如果一个方法调用了其自身的话,我们称之为递归调用。假定栈空间足够的话,尽管递归调用比较难以调试,在Java语言中实现递归调用也是完全可行的。递归方法是众多算法中替代循环的一个不错选择。所有的递归方法都是可重入的,但是不是所有可重入的方法都是递归的。

栈遵守LIFO(Last In First Out)规则,因此递归调用方法能够记住“调用者”并且知道此轮执行结束之返回至当初的被调用位置。递归利用系统栈来存储方法调用的返回地址。 Java是一种基于栈设计的编程语言。

循环的方式可以达到目的,不必采用递归。但是在某些情况下采用递归方式则代码会更加简短易读。递归方法在循环树结构以及避免丑陋的嵌套循环的情况下是非常好用的。

常规递归方法(亦称,头递归)在上面演示了,这种方式会增加调用栈的大小。每次递归,其入口需要被记录在栈中。方法返回之前需要给countA(input.substring(1)的结果加一个count。因此,最后需要做的事其实是加法运算,而非递归本身。

在尾递归中,最后要做的是递归,加法运算在之前就已经完成了。栈调用减少带来了内存消耗减少并且程序的性能更好。如下代码

public class TailRecursiveCall {
 
 public int countA(String input) {
 
  // exit condition – recursive calls must have an exit condition
  if (input == null || input.length() == 0) {
   return 0;
  }
 
  return countA(input, 0) ;
 }
 
 public int countA(String input, int count) {
  if (input.length() == 0) {
   return count;
  }
 
  // check first character of the input
  if (input.substring(0, 1).equals("A")) {
   count = count + 1;
  }
 
  // recursive call is the last call as the count is cumulative
  return countA(input.substring(1), count);
 }
 
 public static void main(String[] args) {
  System.out.println(new TailRecursiveCall().countA("AAA rating"));
 }
}

5. 关于ArrayList

  • ArrayList的大小是如何自动增加的?你能分享一下你的代码吗?

使用ensureCapacity, 在进行添加元素时,检查容量是否足够,不够的话,就将容量扩大3/2,并将旧数组中的元素使用Arrays.copyOf拷贝到新数组中。

  • 什么情况下你会使用ArrayList?什么时候你会选择LinkedList?

ArrayList是在访问的次数远大于插入和删除的次数,使用ArrayList,因为ArrayList底层使用数组,访问的复杂度为O(1), 但是插入和删除就得频繁使用System.arraycopy复制数组。 LinkList主要在访问次数远小于插入和删除的次数时使用,其删除和插入的复杂度,但访问元素时几乎为O(n)。

  • 当传递ArrayList到某个方法中,或者某个方法返回ArrayList,什么时候要考虑安全隐患?如何修复安全违规这个问题呢?

当array被当做参数传递到某个方法中,如果array在没有被复制的情况下直接被分配给了成员变量,那么就可能发生这种情况,即当原始的数组被调用的方法改变的时候,传递到这个方法中的数组也会改变。

将其副本拷贝出来再进行修改。

  • 如何复制某个ArrayList到另一个ArrayList中去?写出你的代码?

使用clone()方法,比如ArrayList newArray = oldArray.clone();

使用ArrayList构造方法,比如:ArrayList myObject = new ArrayList(myTempObject);
使用Collection的copy方法...
  • 注意1和2是浅拷贝(shallow copy),何为浅拷贝?

浅拷贝就比如像引用类型,而深拷贝就比如值类型。浅拷贝是指源对象与拷贝对象共用一份实体,仅仅是引用的变量不同(名称不同)。对其中任何一个对象的改动都会影响另外一个对象。举个例子,一个人一开始叫张三,后来改名叫李四了,可是还是同一个人,不管是张三缺胳膊少腿还是李四缺胳膊少腿,都是这个人倒霉。

深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。举个例子,一个人名叫张三,后来用他克隆(假设法律允许)了另外一个人,叫李四,不管是张三缺胳膊少腿还是李四缺胳膊少腿都不会影响另外一个人。比较典型的就是Value(值)对象,如预定义类型Int32,Double,以及结构(struct),枚举(Enum)等。

还可用序列化技术来进行深拷贝,对象实现序列化接口,然后写入流,并读出来

// 将对象写到流里
        ByteArrayOutputStream bo = new ByteArrayOutputStream();
        ObjectOutputStream oo = new ObjectOutputStream(bo);
        oo.writeObject(this);
        // 从流里读出来
        ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
        ObjectInputStream oi = new ObjectInputStream(bi);
        return (oi.readObject());

但是串行化却很耗时,在一些框架中,我们便可以感受到,它们往往将对象进行串行化后进行传递,耗时较多。

  • 在索引中ArrayList的增加或者删除某个对象的运行过程?效率很低吗?解释一下为什么?

频繁插入和删除,会频繁调用System.arrayCopy....效率低

参考地址
-[1] http://www.importnew.com/2228...
-[2] http://www.importnew.com/2223...
-[3] http://www.importnew.com/2217...
-[4] http://www.importnew.com/2329...
-[5] http://www.importnew.com/9928...
-[6] http://www.cnblogs.com/shuaiw...
-[7] http://blog.csdn.net/biaobiao...

你可能感兴趣的:(java)