Java面试(基础题)-第一篇!
前言
最近准备换工作了,八股文重新复习一边,又有了新的感悟,对于以前不清晰的点,这一次也彻底搞清楚,绝不给面试官留机会。
new String(“abc”)会创建几个对象?
回答
如果字符串常量池中存在abc字面量引用,则只是创建一个对象。如果不存在,就会在创建String对象时,再创建abc字面量对象 并将引用放到常量池中。所以,如果是第二种情况,就会创建两个对象。
扩展
那么,问题来了,第二次 new String(“abc”),他会创建几个对象呢?答案是 三个对象。因为第一次new String(“abc”)的时候,他已经把abc放到常量池中了。所以第二次,只需要创建一个String对象,然后去常量池中拿到abc字面量的引用即可。
再扩展
为什么字符串常量池不能存字面量具体值,而是需要在堆中创建,他自己引用呢。
实际上,在jdk6及之前,字符串常量池是在方法区中的永久代里面的,jdk7及以后,才挪到堆中的。而在方法区中的时候,他确实存的是字面量的值。但是后来发现,这样存,一旦字面量过多,方法区就会被撑大,最后内存溢出GG了。所以,后面挪到堆中,不使用了,没有引用关系了,对象就借助垃圾回收器进行回收,避免OOM。
你知道一个java对象的创建过程是怎么样的吗?
回答
类加载 --> 内存分配 --> 初始化 -->设置对象头 --> 调用init方法
1,JVM在遇到new指令时,类加载器会进行检查类是否被加载。如果被加载,则下一步。没有被加载,则进行类的加载,步骤为,加载,验证,解析,初始化。
2,完成初始化后,就进入了内存分配阶段,由于已经初始化完成,所以此时就已经知道了加载对象的大小。基于对象的大小,去分配内存空间,分配的方式有两种,指针碰撞和空闲列表。JVM基于内存空间是否规则,决定使用哪种方式。内存规则,则使用指针碰撞,不规则,则使用空闲列表,维护一个列表用于记录可用的内存块。
3,初始化零值,为所有默认成员变量赋默认值。int 为0,String 为null,boolean默认为false,保证对象在不显示初始化时,也能使用。
4,设置对象头,Mark Word 存放哈希码,GC分代年龄以及锁状态。Klass Pointer 指向类元数据的指针(JVM通过这个指针判断当前对象所属哪个类) 数组长度(数组对象)
在完成上面这些的时候,对于JVM来说,就已经完成了对象的创建。
5,对于Java来说,此时还需要调用对象的init方法,按照代码逻辑 初始化成员变量。如果有父类对象,则先调用父类的构造方法,完成父类的初始化。
6,将堆中的对象地址 赋给栈中引用变量,此时对象才能真正可用。
扩展
类加载触发时机 除了JVM碰到new字段之外 在遇到静态字段或者方法 以及反射调用(Class.forName())也会触发。
public class Person {private int age = 10;public Person(){this.age = 11;}public int getAge() {return age;}
}
public static void main(String[] args) {Person p= new Person();System.out.println(p.getAge());}
当我们 new Person的时候 就会触发JVM的类加载机制,进行检查,存在则分配内存,不存在则进行加载。内存分配完成后,就会初始化age为0,紧接着 设置对象头,调用构造方法 给age赋值,如果这里不赋值,就会使用默认值,最后 将堆中的内存地址 交给栈的p,让他可以调用。
什么时受检异常,非受检异常?
回答
所谓的受检异常,其实就是在我们编码过程中,需要手动捕捉或者抛出的异常,
非受检异常,则是在编译阶段不会显示报出,只有在运行时出错了,才会报出来 的异常。
扩展
手写一个受检异常和非受检异常给大家看看。
受检异常,注意,下面的main方法中 需要捕捉 或者throws抛出 否则编译就会不通过。
public class CheckedException extends Exception{public CheckedException(String message) {super(message);}public CheckedException(String message, Throwable cause) {super(message, cause);}}public static void main(String[] args) throws Exception {div(1d,0d);}private static double div(double a, double b) throws Exception {if (b == 0) {throw new CheckedException("除数不能为0");}return a / b;}
非受检异常,就是继承Exception的子类RunTimeException,在运行时出错 才会报出来的异常。注意,这里我没有捕捉 或者抛出异常 也可以编译通过,只是在运行时 会抛出错误。
public class CheckedException extends RuntimeException{public CheckedException(String message) {super(message);}public CheckedException(String message, Throwable cause) {super(message, cause);}}public static void main(String[] args) {div(1d,0d);}private static double div(double a, double b) {if (b == 0) {throw new CheckedException("除数不能为0");}return a / b;}
fail-safe,fail-fast是什么?
回答
fail-fast 快速失败,fail-safe 安全失败。
这两种都是为了处理并发操作集合产生的安全性问题。
fail-fast 针对的是 单线程环境下 对集合进行遍历时 向集合中存放或者删除数据,此时就会抛出一个ConcurrentModifictionException错误,代表着遍历失败。常用的集合代表,ArrayList,HashMap,HashSet.
fail-safe 则是多线程环境下,对集合的修改,不会抛出错误 但是迭代的接口不保证包含新修改或者增加的数据。这个本质是就是迭代器操作的时集合的副本 或者某一时刻的快照,而修改的时原集合 因为他是弱一致性的 所以时效性无法保证。 常用的集合代表,ConcurrentHashMap,CopyOnWriteArrayList。
扩展
写一个fail-fast给大家看看。此时,迭代里面想要给集合新增加一个数据,就会抛出ConcurrentModificationException错误,迭代遍历失败。
public static void main(String[] args) {Map<String, Object> map = new HashMap<>();map.put("1", "1");map.put("2", "2");Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator();while (iterator.hasNext()){Map.Entry<String, Object> next = iterator.next();System.out.println(next.getKey() + " " + next.getValue());map.put("3", "3");}}
再写一个fail-safe,此时不会报错,而且 新加入的数据也能打印出来,看起来似乎没有达到我们想要效果。迭代器中存的时集合的副本,那为什么新加入到集合中的数据 也能打印出来呢?
public static void testConcurrentHashMap() {String s = new String("a");ConcurrentHashMap<String, Object> concurrentHashMap = new ConcurrentHashMap<>();//存储一些测试值concurrentHashMap.put("1", "1");concurrentHashMap.put("2", "2");Iterator<Map.Entry<String, Object>> iterator = concurrentHashMap.entrySet().iterator();while (iterator.hasNext()) {Map.Entry<String, Object> next = iterator.next();System.out.println(next.getKey() + " " + next.getValue());concurrentHashMap.put("4", "4");}}
这是因为,集合的本质 其实就是数据+链表+红黑树,我们知道 数组默认时16,达到它0.75也就是14的时候 他就会扩容,在此之前 他不扩容,且采用的是节点上的分段锁来进行管理的。也就是说 我们给他12个数据 再迭代时 新增数据 他就无法操作到了。
public static void testConcurrentHashMap() {String s = new String("a");ConcurrentHashMap<String, Object> concurrentHashMap = new ConcurrentHashMap<>();//存储一些测试值for (int i = 0; i < 12; i++) {concurrentHashMap.put(String.valueOf(i), i);}Iterator<Map.Entry<String, Object>> iterator = concurrentHashMap.entrySet().iterator();while (iterator.hasNext()) {Map.Entry<String, Object> next = iterator.next();System.out.println(next.getKey() + " " + next.getValue());concurrentHashMap.put("17", "17");}System.out.println("==============================================================");Iterator<Map.Entry<String, Object>> iterator1 = concurrentHashMap.entrySet().iterator();while (iterator1.hasNext()) {Map.Entry<String, Object> next = iterator1.next();System.out.println(next.getKey() + " " + next.getValue());}}
HashMap如何解决哈希冲突?
回答
首先 我们需要知道,HashMap的key 时通过哈希算法将输入任意长度的值,转化为输出一个固定长度的值,我们输入的东西时无限的,但是输出是有限的,所以 就会产生重复的key值。
为了解决这个问题,HashMap采用的是链式寻址法,以单项链表的方式,在数组的节点下面挂上链表,进行存储。如果超过8位,且数据长度超过64则转变为红黑树,增加查询效率。
扩展
那除了 链式寻址法之外 还有其他方法可以解决哈希冲突吗?
当然是有的。
就比如,ThreaLocal使用的开发定址法,也称之为 线性探测法,从冲突的位置,按照一定的次序,找到一个合适的位置,进行存储。
还有 再哈希,就是重复的key,再进行哈希运算,直到得出一个不重复的key,但这样做 就比较耗费性能,毕竟,本来应该一次算出来的东西,多次通过散列表运算。
还可以 建立公共区 和溢出区 公共区存放正常的数据,溢出区则存储冲突的数据。
JDK动态代理,为什么只能代理有接口的类?
回答
JDK动态代理 是通过反射和接口实现的,代理类需要继承Proxy类,我们知道,Java是单继承的,所以,继承了Proxy后,就无法再继承其他类类,只能实现接口。
JDK动态代理的初衷,是面向接口编程,推荐使用接口解耦合。如果想要代理类,可以使用CGLIB来进行代理。Spring AOP就是采用JDK动态代理。
扩展
写一个JDK动态代理的例子 给大家看看。在Proxy.newProxyInstance代理接口的时候 运行时 就会生成一个Proxy0继承Proxy然后实现这个接口。
public interface UserService {void addUser(String name);void deleteUser(String id);
}
public class UserServiceImpl implements UserService{@Overridepublic void addUser(String name) {System.out.println("add user: " + name);}@Overridepublic void deleteUser(String id) {System.out.println("delete user: " + id);}
}
public class UserProxy {public static UserService createUserProxy(UserService userService) {return (UserService) Proxy.newProxyInstance(UserService.class.getClassLoader(), new Class[]{UserService.class}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("before");Object result = method.invoke(userService, args);System.out.println("after");return result;}});}}public static void main(String[] args) {UserServiceImpl userService = new UserServiceImpl();UserService userServiceProxy = UserProxy.createUserProxy(userService);userServiceProxy.addUser("tongz");}
再看看CGLIB动态代理。
public class CglibProxyFactory {public static UserService createUserServiceProxy() {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(UserServiceImpl.class);enhancer.setCallback(new MethodInterceptor() {@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {System.out.println("before");System.out.println("method:" + method.getName());Object result = methodProxy.invokeSuper(o, objects);System.out.println("after");return result;}});return (UserService) enhancer.create();}
}public static void main(String[] args) {UserService userServiceProxy = CglibProxyFactory.createUserServiceProxy();userServiceProxy.addUser("tongz");}
String StringBuffer StringBuilder
回答
不可变性 String 它的value是final修饰的,不可变。每次修改String的值 都会产生一个新的对象。StringBuilder 和 StringBuffer是可变更的 不会产生新的类。
线程安全性来说 String 不可变 是线程安全的。StringBuffer也是线程安全的,每个操作方法
都有一个synchronized关键字修饰。StringBuilder不安全。
性能上 StringBuilder最好 其次 StringBuffer 再次 String,因为String每次修改都要创建新的对象,而StringBuffer则不需要创建对象 但是他加了同步锁,影响性能。
内存上 String 引用存储在字符串常量池 StringBuffer和StringBuilder存储在堆内存空间。
扩展
没啥好扩展的,看一下StringBuffer的源码吧。方法上都有synchronized修饰。