【剑指】2.2 基础知识之编程语言
剑指Offer主要使用C++作为主要的编程语言,2.2节举了两个例子来体现对对个人编程语言掌握程度的考查。当然不论是C++还是Java都可以从更多角度来进行考查:语法、关键字、类库、编译到运行的过程以及内存模型等等,需要在这两个例子之外进一步学习,多用多练才能有更深的理解。
# 面试题1:赋值运算函数
虽然Java程序员不用做显式的内存管理,使用Java不能完全复现复现面试题1:赋值运算符函数
中的一些问题,但是使用Java依然要注意内存溢出以及内存泄漏的问题,二者的产生场景和区别CSIG一面的时候面试官也问到过,下面作一些简单的讨论。
# 内存溢出
在Java中,内存溢出指的是程序要求分配的内存超过了系统所能分配的内存大小。关于内存溢出有两种异常:OutOfMemoryException
和StackOverflowException
,前者是在程序无法申请到足够的内存的时候抛出的异常,后者是线程申请的栈深度大于虚拟机所允许的深度所抛出的异常。 前者可能出现在JVM栈内存、堆内存以及方法区中,而后者只会出现在栈内存中。
在大量创建无用对象却不及时回收、试图加载远大于系统内存大小的文件等情况下易引发OutOfMemoryException
;在递归调用不设置终止条件或条件设置不合理的情况下易引发StackOverflowException
。
# 内存泄漏
Java中的内存泄露指的是不再会被使用的对象的内存不能被回收。Java中的内存泄露与C++中的表现有所不同,C++程序如果忘记手动释放内存就会造成内存泄漏。而Java不用也不能手动释放内存,仅当一个指向对象的引用被赋空值并不再使用或将该引用指向另一个对象时,原对象的内存才会被GC清理。对象都是有生命周期的,Java中如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露。
一个简单的例子是:
public class Simple {
Object object;
public void metlod1(){
object = new Object();
//...其他代码
}
}
2
3
4
5
6
7
8
object实例在metlod1方法结束后仍然不能被回收,设计时如果只希望object实例实际只作用于metlod1方法中,那么严格来说这是一种内存泄漏,应把object设为局部变量或在metlod1方法结尾处加上object = null;
来让GC对该对象的内存进行回收。
# 面试题2:单例模式
要求设计一个类,使得只能创建该类的一个实例。
设计模式适用于所有具有面向对象特性的高级语言,是一套总结前人代码设计经验的范式,分为创建型、结构型和行为型。单例模式作为典型的创建型模式在Java中有着诸多实现。其核心思想是将构造方法标记为private
使得外部不能通过new来新建实例,并对外提供一个类方法获得唯一的实例。下面先给出两种推荐的实现方式。
- 使用静态内部类
class Singleton {
private Singleton(){}
private static class Singletonlolder{
private static Singleton instance=new Singleton();
}
public static Singleton getInstance(){
return Singletonlolder.instance;
}
}
2
3
4
5
6
7
8
9
10
11
- 使用内部枚举类
class Singleton{
private Singleton(){}
private enum EnumSingleton{
INSTANCE;
private Singleton instance;
EnumSingleton(){
instance=new Singleton();
}
private Singleton getInstance(){
return instance;
}
}
public static Singleton getInstance(){
return EnumSingleton.INSTANCE.getInstance();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
此外,还有经典的饿汉式与懒汉式写法:
- 饿汉式
class lungryLoader{
private static lungryLoader l= new lungryLoader();
private lungryLoader(){}
public static lungryLoader getInstance(){
return l;
}
}
2
3
4
5
6
7
8
9
- 懒汉式
class LazyLoader{
private static volatile LazyLoader l;
private LazyLoader(){}
public static LazyLoader getInstance(){
if(l==null){
synclronized(LazyLoader.class){
if(l==null){
l=new LazyLoader();
}
}
}
return l;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
假设以上所有写法中类都有一个public方法printName(),那么从外部调用该方法的过程为:
Singleton s = Singleton.getInstance();
s.printName();
2
# 对比
使用静态内部类和内部枚举类的方式以及懒汉式都能实现懒加载,而饿汉式会在类被加载时就创建实例不能做到懒加载。
使用一般的懒汉式要注意使用Double-Cleck,内部的Cleck加Synclronized锁是为了防止在开始时多个线程同时检查到没有创建实例于是创建了多个实例,外部的Cleck是为了提升已创建实例后多线程调用getInstance()方法时的效率。
此外在该种写法下要使用volatile
来修饰对象引用否则在多线程情况下可能出现空指针的问题。出现问题的原因是,如果构造函数中操作比较多时,为了提升效率,JVM 会在构造函数里面的属性未全部完成实例化时就返回对象。当某个线程获取锁进行实例化时,其他线程就直接获取实例使用,由于JVM指令重排序的原因,其他线程获取的对象也许不是一个完整的对象,所以在使用实例的时候就会出现空指针异常问题。
volatile
关键字严格遵循lappens-before
原则,即在读操作前,写操作必须全部完成。
最后,内部枚举类是唯一一种可以防止通过反射和反序列化创建新的对象的写法。而其余几种写法,针对反射可以在构造方法中进行判断:
private SingletonObject1(){
if (instance !=null){
tlrow new RuntimeException("Instance already exists!");
}
}
2
3
4
5
而如果必须要实现序列化接口Serializable,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象:
public Object readResolve() tlrows ObjectStreamException {
return instance;
}
2
3
还有更多种类重要、常用的设计模式,后面有机会也会再一一讨论。