1. Java的安全性
- 使用引用取代了指针,指针的功能强大,但是也容易造成错误,如数组越界问题。
- 拥有一套异常处理机制,使用关键字 throw、throws、try、catch、finally
- 不用程序员显示控制内存释放,JVM 有垃圾回收机制
- 强制类型转换需要符合一定规则
- 字节码传输使用了加密机制
- 运行环境提供保障机制:字节码校验器->类装载器->运行时内存布局->文件访问限制
2. Java三大特性
- 封装
- 封装指的是属性私有化,根据需要提供setter和getter方法来访问属性。即隐藏具体属性和实现细节,仅对外开放接口,控制程序中属性的访问级别。
- 封装目的:增强安全性和简化编程,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员。
- 继承
- 继承是指将多个相同的属性和方法提取出来,新建一个父类。Java中一个类只能继承一个父类,且只能继承访问权限非private的属性和方法。 子类可以重写父类中的方法,命名与父类中同名的属性。
- 继承目的:代码复用。
- 多态
- 多态可以分为两种:设计时多态和运行时多态。
- 设计时多态:即重载,是指Java允许方法名相同而参数不同(返回值可以相同也可以不相同)。
- 运行时多态:即重写,是指Java运行根据调用该方法的类型决定调用哪个方法。要求方法名、参数和返回值必须相同。
- 多态目的:增加代码的灵活度。
3. 多态程序绑定
- 定义:绑定指的是一个方法的调用与方法所在的类或对象(方法主体)关联起来。对java来说,绑定分为静态绑定和动态绑定
- 静态绑定(前期绑定):在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。在编译阶段,绑定的是类信息,即为定义的类的类型。针对java简单的可以理解为程序编译期的绑定;这里特别说明一点,java当中的方法,只有final,static,private,重载方法(overloaded methods)和构造方法是静态绑定。所有的变量都是静态绑定。
- 动态绑定(后期绑定):在运行时根据具体对象的类型进行绑定。发生在运行阶段,绑定的是对象信息。重写方法(overridden methods)使用的是动态绑定
- 动态绑定的过程:
1)虚拟机提取对象实际类型的方法表;
2)虚拟机搜索方法签名;
3)调用方法。
4. Java四种引用类型
- 强引用:Java中默认声明的就是强引用,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了
- 软引用:软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
- 弱引用:弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。(GC时发生)
- 虚引用:虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,主要用来跟踪对象被垃圾回收的活动。
5. 不可变对象
- 不可变对象:对象在创建完成后,不能再改变它的状态。即不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。
- 不可变对象有什么好处?
1)不可变对象可以提高String Pool(字符串常量池)的效率和安全性。如果你知道一个对象是不可变动,那么需要拷贝的对象的内容时就不用复制它本身而只是复制它的地址,复制地址(通常一个指针的大小)需要很小的内存,效率也很好。二是对于其他引用同一个对象的其他变量也不会造成影响。
2)不可变对象对于多线程是安全的,因为在多线程同时进行的情况下,一个可变对象的值很可能被其他线程改变,这样会造成不可预期的结果,而使用不可变对象就可以避免这种情况出现。
6. String
- String是否可变?
Java中String类就是对字符数组的封装。Jdk8中String类有两个成员变量char value[]和int hash,value是private final的,hash被private修饰,也就是说在String类内部,一旦初始化就不能被改变。所以可以认为String对象是不可变的。 - String为什么要设计为不可变?
java将String设成不可变最大的原因是效率和安全。
1)字符串常量池的需要,只有字符串不可变时,字符串常量池才能实现。
2)多线程安全
3)字符串不变性保证了hash码的唯一性,因此可以放心的进行缓存,这也是一种性能优化手段,意味着不必每次都重新计算新的哈希码,使得字符串很适合作为 Map的键
4)类加载器要用到字符串,不可变提供了安全性,以便类被正确地加载。
5)String被许多的Java类(库)用来当做参数,例如网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。 - String对象真的不可变吗?
用反射,可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。可以通过类对象的getDeclaredField()方法字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了。
7. Java创建对象的4种方式
- 使用new关键字:new关键字直接在堆内存上创建对象。
- 反射:使用Class类的newInstance方法可以调用无参的构造器来创建对象,如果是有参构造器,则需要使用Class的forName方法和Constructor来进行对象的创建。
- 使用Clone方法:调用一个对象的clone方法,JVM就会创建一个新的对象,将前面的对象的内容全部拷贝进去,用clone方法创建对象并不会调用任何构造函数。
- 反序列化:一个对象实现了Serializable接口,就可以把对象写入到文件中,并通过读取文件来创建对象。
8. 反射
JAVA语言编译之后会生成一个.class文件,反射就是通过字节码文件找到某一个类、类中的方法以及属性等。反射机制指的是程序在运行时能够获取自身的信息。在JAVA中,只要给定类的名字,那么就可以通过反射机制来获取类的所有信息。Java 的动态就体现在反射。通过反射我们可以实现动态装配,降低代码的耦合度;动态代理等。反射的过度使用会严重消耗系统资源。
反射的实现主要借助以下四个类:Class:类的对象,Constructor:类的构造方法,Field:类中的属性对象,Method:类中的方法对象。
- 反射的作用:
1)可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型
2)应用程序需要在运行时从某个特定的程序集中载入一个特定的类型,以便实现某个任务时可以用到反射。
3)反射主要应用于类库,这些类库需要知道一个类型的定义,以便提供更多的功能。
9. StringBuffer和StringBuilder
- 相同点:StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。底层实现上的话,StringBuffer其实就是比StringBuilder多了Synchronized修饰符。
- 区别:
1)StringBuilder 的方法不是线程安全的(不能同步访问)
2)StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。 - 小结:
1)如果要操作少量的数据用 String;
2)多线程操作字符串缓冲区下操作大量数据 StringBuffer;
3)单线程操作字符串缓冲区下操作大量数据 StringBuilder。
10. java.lang.Object的常用方法
- getClass() 获取类结构信息
- toString() 把对象转变成字符串
- hashCode() 获取哈希码
- equals(Object) 默认比较对象的地址值是否相等,子类可以重写比较规则
- notify() 多线程中唤醒功能
- notifyAll() 多线程中唤醒所有等待线程的功能
- wait()让持有对象锁的线程进入等待
- wait(long timeout)让持有对象锁的线程进入等待,设置超时毫秒数时间
- wait(long timeout, int nanos)让持有对象锁的线程进入等待,设置超时纳秒数时间
11. 为什么Java把wait与notify放在Object中?
- 功能角度
1)wait与notify的原始目的,是多线程场景下,某条件触发另一逻辑,该条件对应的直接关系为某种对象,进而对应为Object,其对应为内存资源。
2)Thread对应为CPU,与具体条件不是直接关系,Thread是对象的执行依附者。 - 内存角度
1)线程的同步需要Monitor的管理,其与实际操作系统的重型资源(锁)相关。
2)只有涉及多线程的场景,才需要线程同步,如果wait与notify放在Thread,则每个Thread都需要分配Monitor,浪费资源。
3)如果放在Object,单线程场景不分配Monitor,只在多线程分配。分配Monitor的方法为检测threadId的不同。
12. 装箱和拆箱
装箱是通过调用包装器类的 valueOf 方法实现的;拆箱是通过调用包装器类的 xxxValue 方法实现的,xxx代表对应的基本数据类型。如int装箱的时候自动调用Integer的valueOf(int)方法;Integer拆箱的时候自动调用Integer的intValue方法。包含算术运算会触发自动拆箱。存在大量自动装箱的过程,如果装箱返回的包装对象不是从缓存中获取,会创建很多新的对象,比较消耗内存。
- 整型的包装类 valueOf 方法返回对象时,在常用的取值范围内,会返回缓存对象。
- 浮点型的包装类 valueOf 方法返回新的对象。
- 布尔型的包装类 valueOf 方法 Boolean类的静态常量 TRUE | FALSE。
13. Integer和String的比较操作
- 使用 == 比较:
- 基本类型 - 基本类型、基本类型 - 包装对象返回 true
- 包装对象 - 包装对象,非同一个对象(对象的内存地址不同)返回 false;对象的内存地址相同返回 true,如值等于 100 的两个 Integer 对象(原因是 JVM 缓存部分基本类型常用的包装类对象,如 Integer -128 ~ 127 是被缓存的)
- 使用 equals() 比较
- 包装对象-基本类型返回 true
- 包装对象-包装对象返回 true
Integer a = 1;
Integer b = 1;
Integer c = 128;
Integer d = 128;
// [-128,127]范围的自动装箱(box),同值是同一个对象
System.out.println(a == b); // true
// 不在[-128,127]范围装箱的Integer,值相同也不是同一个对象
System.out.println(c == d); // false
// 使用new一个对象的方法
Integer a = new Integer(1);
Integer b = new Integer(1);
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
System.out.println(a.equals(1)); // true
System.out.println(a == 1); // true, Integer和int用==比较,Integer自动拆箱unbox,转换为普通int间的比较
/**
* String的比较,==是引用比较,比较字符串是否相同用equals
*/
//用引号创建一个字符串的时候,首先会去常量池中寻找有没有相等的常量对象,没有的话就在常量池中创建这个常量对象;有的话就直接返回这个常量对象的引用
String str1 = "haha";
String str2 = "haha";
String str3 = new String("haha");
String str4 = new String("haha");
// true,首先 String str1 = "hello",会先到常量池中检查是否有“hello”的存在,发现是没有的,于是在常量池中创建“hello”对象,并将常量池中的引用赋值给str1;第二个字面量 String str2 = "hello",在常量池中检测到该对象了,直接将引用赋值给str2。
System.out.println(str1 == str2);
// false, 每个String对象都是不同的,所以引用指向的堆地址肯定也不同,所以false。
System.out.println(str3 == str4);
// false,因为==比较的是引用的地址,s2指的是常量池中常量对象的地址,而s1指的是堆中String对象的地址,肯定不同。
System.out.println(str1 == str3);
// true,因为jdk重写了equals()方法,比较的是字符串的内容。
System.out.println(str1.equals(str2));
//true, JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。
System.out.println(str3.intern()==str1);
14. 动态链接库和静态链接库
- 静态链接库:当要使用时,连接器会找出程序所需的函数,然后将它们拷贝到执行文件,由于这种拷贝是完整的,所以一旦连接成功,静态程序库也就不再需要了。
- 动态链接库:某个程序在运行中要调用某个动态链接库函数的时候,操作系统首先会查看所有正在运行的程序,看在内存里是否已有此库函数的拷贝了。如果有,则让其共享那一个拷贝;只有没有才链接载入。在程序运行的时候,被调用的动态链接库函数被安置在内存的某个地方,所有调用它的程序将指向这个代码段。因此,这些代码必须使用相对地址,而不是绝对地址。在编译的时候,我们需要告诉编译器,这些对象文件是用来做动态链接库的,所以要用地址无关代码(Position Independent Code (PIC))。动态链接库的加载方式有两种:隐式加载和显示加载。
15. 正则表达式
- 定义:在编写处理字符串的程序时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。
- Java中的String类提供了支持正则表达式操作的方法,包括:matches()、replaceAll()、replaceFirst()、split()。
16. Java基本数据类型及其包装类
Java 为每个原始类型提供了包装类型:
- 原始类型:boolean,char,byte,short,int,long,float,double
- 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
17. 值传递和引用传递
一般认为,java内的传递都是值传递
- 值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量。
- 引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本, 并不是原对象本身。所以对引用对象进行操作会同时改变原对象。
18. 深拷贝和浅拷贝
- 浅拷贝:复制基本类型的属性、引用类型的属性、栈中的变量和变量指向堆内存中的对象的指针,不复制堆内存中的对象。
- 深拷贝:复制基本类型的属性、引用类型的属性、栈中的变量和变量指向堆内存中的对象的指针和堆内存中的对象。
19. 为什么会出现4.0-3.6=0.40000001这种现象?
2进制的小数无法精确的表达10进制小数,计算机在计算10进制小数的过程中要先转换为2进制进行计算,这个过程中出现了误差。
20. 十进制的数在内存中是怎么存的?
补码的形式
- 正数的原反补一样
- 负数的反码是将原码除了符号位的其余位取反,补码是给反码加1.
21. Lamda表达式
- 定义:Lambda 表达式(lambda expression)是一个匿名函数,Lambda 规定接口中只能有一个需要被实现的方法,不是规定接口中只能有一个方法
- 思想:函数式编程思想
- 优点:1. 简洁。2. 非常容易并行计算。
- 缺点:1. 若不用并行计算,很多时候计算速度没有比传统的 for 循环快。(并行计算有时需要预热才显示出效率优势)2. 不容易调试。
22. Java 8系列之Stream
Stream 是用函数式编程方式在集合类上进行复杂操作的工具,其集成了Java 8中的众多新特性之一的聚合操作,开发者可以更容易地使用Lambda表达式,并且更方便地实现对集合的查找、遍历、过滤以及常见计算等。
- Stream的操作分类,在一次聚合操作中,可以有多个Intermediate,但是有且只有一个Terminal。Intermediate主要是用来对Stream做出相应转换及限制流,实际上是将源Stream转换为一个新的Stream,以达到需求效果。
- Intermediate:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 skip、 parallel、 sequential、 unordered
- Terminal:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、iterator
- Short-circuiting:anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
23. final关键字
- 使用final的原因:第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。
- 作用:
1)当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。
2)对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;
3)如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
24. final/finally/finalize
- final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。
- finally是异常处理语句结构的一部分,表示总是执行。
- finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。
25. Java中的IO流
java.io包中还有许多其他的流,主要是为了提高性能和使用方便。
- 按照流向划分为输入流和输出流
- 按照操作单元分划分为字节流和字符流
- 字节流:InputStream、OutputStream
- 字符流:InputStreamReader、OutputStreamWriter
- 按照流的角色划分为节点流和处理流
26. 既然有了字节流,为什么还要有字符流?
字符流是由 Java 虚拟机将字节转换得到的,这个过程非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
27. java序列化
- 定义:序列化是将 Java 对象转换成字节流的过程。反序列化是将字节流转换成 Java 对象的过程。
- 作用:当Java对象需要在网络上传输或者持久化存储到文件时,就需要对Java对象进行序列化处理。
- 实现:类实现 Serializable 接口,这个接口没有需要实现的方法。实现 Serializable 接口是为了告诉 jvm 这个类的对象可以被序列化。
28. Java 序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用 transient 关键字修饰。transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。
29. 泛型
- 定义:泛型,即“参数化类型”。将类型作为参数传入方法中,如List<String>。
- 优点:在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
- Java泛型的实现方法:类型擦除
Java的泛型是伪泛型,因为Java在编译期间,所有的泛型信息都会被擦掉。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程称为类型擦除。
30. 抽象类和接口
- 声明方法的存在而不去实现它的类被叫做抽象类(abstract class),它用于要创建一个体现某些基本行为的类,并为该类声明方法,但不能在该类中实现该类的情况。不能创建abstract 类的实例。然而可以创建一个变量,其类型是一个抽象类,并让它指向具体子类的一个实例。不能有抽象构造函数或抽象静态方法。Abstract 类的子类为它们父类中的所有抽象方法提供实现,否则它们也是抽象类。取而代之,在子类中实现该方法。知道其行为的其它类可以在类中实现这些方法。
- 接口(interface)是抽象类的变体。在接口中,所有方法都是抽象的。多继承性可通过实现这样的接口而获得。接口中的所有方法都是抽象的,没有一个有程序体。接口只可以定义static final成员变量。接口的实现与子类相似,除了该实现类不能从接口定义中继承行为。当类实现特殊接口时,它定义(即将程序体给予)所有这种接口的方法。然后,它可以在实现了该接口的类的任何对象上调用接口的方法。由于有抽象类,它允许使用接口名作为引用变量的类型。通常的动态联编将生效。引用可以转换到接口类型或从接口类型转换,instanceof 运算符可以用来决定某对象的类是否实现了接口。
- 区别:
1)接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。
2)类可以实现很多个接口,但是只能继承一个抽象类
3)Java接口中声明的变量默认都是final的。抽象类可以包含非final的变量。
4)Java接口中的成员函数默认是public的。抽象类的成员函数可以是private,protected或者是public。
5)抽象类可以在不提供接口方法实现的情况下实现接口。
6)类可以不实现抽象类和接口声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。
7)接口是绝对抽象的,不可以被实例化。抽象类也不可以被实例化,但是,如果它包含main方法的话是可以被调用的。
31. java类的里面可以再定义一个类吗
java类里面还可以定义一个类,即内部类。java内部类分为: 成员内部类、方法(局部)内部类、静态内部类、匿名内部类 。
- 成员内部类:
- 成员内部类可以无条件访问外部类的属性和方法,但是外部类想要访问内部类属性或方法时,必须要创建一个内部类对象,然后通过该对象访问内部类的属性或方法
- 局部内部类
- 局部内部类存在于方法中。
- 他和成员内部类的区别在于局部内部类的访问权限仅限于方法或作用域内。
- 静态内部类:
- 静态内部类和成员内部类相比多了一个static修饰符。只能访问外部类的静态成员变量与静态方法。
- 静态内部类的非静态成员可访问外部类的静态变量,而不可访问外部类的非静态变量。
- 匿名内部类:
- 没有类名,没有class关键字也没有extends和implements等关键字修饰。唯一没有构造方法的内部类。
- 类的定义和对象的实例化同时进行。
- 内部类的好处
- 完善了Java多继承机制,由于每一个内部类都可以独立的继承接口或类,所以无论外部类是否继承或实现了某个类或接口,对于内部类没有影响。
- 方便写事件驱动程序。
32. &和&&、|和||
- &:逻辑与,& 两边的表达式都会进行运算
- &&:短路与,&& 左边的表达式结果为 false 时,&& 右边的表达式不参与计算
- |:逻辑或,| 两边的表达式都会进行运算
- ||:短路或,|| 左边的表达式结果为 true 时,|| 右边的表达式不参与计算
33. 不使用任何中间变量,交换a,b两个数字的值
- a=a+b;b=a-b;a=a-b;(a+b可能越界)
- a = a ^ b;b = a ^ b;a = a ^ b;
34. 常见异常
常见异常类及其父子关系:
Throwable
| ├ Error
| │ ├ IOError
| │ ├ LinkageError
| │ ├ ReflectionError
| │ ├ ThreadDeath
| │ └ VirtualMachineError
| │
| ├ Exception
| │ ├ CloneNotSupportedException
| │ ├ DataFormatException
| │ ├ InterruptedException
| │ ├ IOException
| │ ├ ReflectiveOperationException
| │ ├ RuntimeException(不需要代码显式捕获处理)
| │ ├ ArithmeticException
| │ ├ ClassCastException
| │ ├ ConcurrentModificationException
| │ ├ IllegalArgumentException
| │ ├ IndexOutOfBoundsException
| │ ├ NoSuchElementException
| │ ├ NullPointerException
| │ └ SecurityException
| │ └ SQLException
运行时异常都是 RuntimeException 子类异常
- NullPointerException - 空指针异常
- ClassCastException - 类转换异常
- IndexOutOfBoundsException - 下标越界异常
- ArithmeticException - 计算异常
- IllegalArgumentException - 非法参数异常
- NumberFormatException - 数字格式异常
- UnsupportedOperationException 操作不支持异常
- ArrayStoreException - 数据存储异常,操作数组时类型不一致
- BufferOverflowException - IO 操作时出现的缓冲区上溢异常
- NoSuchElementException - 元素不存在异常
- InputMismatchException - 输入类型不匹配异常
- ConcurrentModificationException – 并发修改异常
35. 设计模式
总体来说设计模式分为三大类(25种):
- 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
- 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
- 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
- 其实还有两类:并发型模式和线程池模式。
36. 设计模式的六大原则
总原则:开闭原则,开闭原则就是说对扩展开放,对修改关闭。
- 单一职责原则:每个类应该实现单一的职责,如若不然,就应该把类拆分。
- 里氏替换原则:任何基类可以出现的地方,子类一定可以出现。
- 依赖倒转原则:面向接口编程,依赖于抽象而不依赖于具体。
- 接口隔离原则:每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。
- 最少知道原则:一个类对自己依赖的类知道的越少越好。
- 合成复用原则:尽量首先使用合成/聚合的方式,而不是使用继承。
37. 单例模式
- 定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
- 作用:解决一个全局使用的类频繁地创建与销毁。
- 主要优点:
- 提供了对唯一实例的受控访问。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。
- 主要缺点:
- 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
- 分类:
1)饿汉式(线程安全):在调用getInstance()方法前就初始化instance实例- 优点:没有加锁,执行效率会提高。
- 缺点:类加载时就初始化,浪费内存。
public class Singleton {
//在类的内部创建一个类的实例,且为static
private static Singleton instance = new Singleton();
//私有化构造器
private Singleton (){}
//此公共方法只能通过类来调用,因为设置的是static
public static Singleton getInstance() {
return instance;
}
}
2)懒汉式(线程不安全):调用getInstance()方法时才创建instance实例
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
解决方法:
a) 加Synchorized锁
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
b) 双端检锁(加锁前后都进行判断)
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
c) 静态内部类
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
d) 枚举是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
38. 生产者消费者模式
生产者消费者问题是线程模型中的经典问题:生产者和消费者在同一时间段内共用同一存储空间,生产者向空间里生产数据,而消费者取走数据。优点:支持并发、解耦。