Java知识点(面试准备)总结
- 1. 需要掌握的内容
- 2. 常见问题
- 3. 数据类型
- 4. Java反射
- 5. 变量
- 6. 注解
- 7. 泛型与枚举
- 8. 类与接口
- 9. Overload和Override的区别,即重载和重写的区别。
- 10. 集合
- 10.1. Java集合框架的基础接口有哪些?
- 10.2. 当一个集合被作为參数传递给一个函数时,怎样才干够确保函数不能改动它?
- 10.3. Collections
- 10.4. Map
- 10.5. Collection
- 10.6. Iterator
- 11. 多线程
- 12. I/O流
- 13. NIO
- 14. 动态代理
- 15. 异常
- 16. 常用API
- 17. JDBC
- 18. 设计模式
- 19. 六大原则
- 20. 框架
参考:
面试题 / J2SE / J2SE JAVA 面试题
Java面试2018常考题目汇总(一)
1. 需要掌握的内容
多线程,并发及线程基础
数据类型转换的基本原则
垃圾回收(GC)
Java 集合框架
数组
字符串
GOF 设计模式
SOLID (单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)设计原则
抽象类与接口
Java 基础,如 equals 和 hashcode
泛型与枚举
Java IO 与 NIO
常用网络协议
Java 中的数据结构和算法
正则表达式
JVM 底层
Java 最佳实践
JDBC
Date, Time 与 Calendar
Java 处理 XML
JUnit
编程
2. 常见问题
2.1. 什么是值传递和引用传递?
- 值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量.
- 引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本(实际上也可以看做基本类型), 并不是原对象本身 。
- 一般认为,java内的传递都是值传递. java中实例对象的传递是引用传递 。
2.2. 在JAVA中,如何跳出当前的多重嵌套循环
在外部循环的前一行,加上标签
在break的时候使用该标签
即能达到结束多重嵌套循环的效果
public class HelloWorld {
public static void main(String[] args) {
//打印单数
outloop: //outloop这个标示是可以自定义的比如outloop1,ol2,out5
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
System.out.println(i+":"+j);
if(0==j%2)
break outloop; //如果是双数,结束外部循环
}
}
}
}
2.3. equals与hashCode
在Object类的equals()方法中有这么一段话
- Note that it is generally necessary to override the {@code hashCode}
- method whenever this method is overridden, so as to maintain the
- general contract for the {@code hashCode} method, which states
- that equal objects must have equal hash codes.
翻译如下:
通常来讲,在重写这个方法的时候,也需要对hashCode方法进行重写,
以此来保证这两个方法的一致性——
当equals返回true的时候,这两个对象一定有相同的hashcode.
2.3.1. 两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?
因为hashCode()方法和equals()方法都可以通过自定义类重写,是可以做到equals相同,但是hashCode不同的
2.3.2. hashCode()和equals()方法有何重要性?
hashCode() 方法是相应对象整型的 hash 值。它常用于基于 hash 的集合类,如 Hashtable、HashMap、LinkedHashMap等等。
根据 Java 规范,两个使用 equal() 方法来判断相等的对象,必须具有相同的 hash code。
HashMap使用Key对象的hashCode()和equals()方法去决定key-value对的索引。当我们试着从HashMap中获取值的时候,这些方法也会被用到。如果这些方法没有被正确地实现,在这种情况下,两个不同Key也许会产生相同的hashCode()和equals()输出,HashMap将会认为它们是相同的,然后覆盖它们,而非把它们存储到不同的地方。同样的,所有不允许存储重复数据的集合类都使用hashCode()和equals()去查找重复,所以正确实现它们非常重要。equals()和hashCode()的实现应该遵循以下规则:
- (1)如果o1.equals(o2),那么o1.hashCode() == o2.hashCode()总是为true的。
- (2)如果o1.hashCode() == o2.hashCode(),并不意味着o1.equals(o2)会为true。
2.3.3. “a==b”和”a.equals(b)”有什么区别?
如果 a 和 b 都是对象,则 a==b 是比较两个对象的引用,只有当 a 和 b指向的是堆中的同一个对象才会返回 true,而 a.equals(b) 是进行逻辑比较,所以通常需要重写该方法来提供逻辑一致性的比较。例如,String 类重写 equals() 方法,所以可以用于两个不同对象,但是包含的字母相同的比较。
2.4. Comparable和Comparator接口
2.4.1. Comparable
该接口只定义了一个方法compareTo(O o)
引自jdk api:
此接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo 方法被称为它的自然比较方法。
对于类 C 的每一个 e1 和 e2 来说,当且仅当 e1.compareTo(e2) == 0 与 e1.equals(e2) 具有相同的 boolean 值时,类 C 的自然排序才叫做与 equals 一致。注意,null 不是任何类的实例,即使 e.equals(null) 返回 false,e.compareTo(null) 也将抛出 NullPointerException。
compareTo(O o)比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。
Comparable是排序接口。若一个类实现了Comparable接口,就意味着该类支持排序。实现了Comparable接口的类的对象的列表或数组可以通过Collections.sort或Arrays.sort进行自动排序。
此外,实现此接口的对象可以用作有序映射中的键或有序集合中的集合,无需指定比较器。
public class Person implements Comparable<Person>
{
String name;
int age;
public Person(String name, int age)
{
super();
this.name = name;
this.age = age;
}
public String getName()
{
return name;
}
public int getAge()
{
return age;
}
@Override
public int compareTo(Person p)
{
return this.age-p.getAge();
}
public static void main(String[] args)
{
Person[] people=new Person[]{new Person("xujian", 20),new Person("xiewei", 10)};
System.out.println("排序前");
for (Person person : people)
{
System.out.print(person.getName()+":"+person.getAge());
}
Arrays.sort(people);
System.out.println("\n排序后");
for (Person person : people)
{
System.out.print(person.getName()+":"+person.getAge());
}
}
}
结果
排序前
xujian:20xiewei:10
排序后
xiewei:10xujian:20
2.4.2. Comparator
该接口定义了两个个方法:int compare(T o1, T o2)和equals
比较用来排序的两个参数。根据第一个参数小于、等于或大于第二个参数分别返回负整数、零或正整数。
引自jdk api:
强行对某个对象 collection 进行整体排序 的比较函数。可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用 Comparator 来控制某些数据结构(如有序 set或有序映射)的顺序,或者为那些没有自然顺序的对象 collection 提供排序。
当且仅当对于一组元素 S 中的每个 e1 和 e2 而言,c.compare(e1, e2)==0 与 e1.equals(e2) 具有相等的布尔值时,Comparator c 强行对 S 进行的排序才叫做与 equals 一致 的排序。
Comparator是比较接口,我们如果需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable接口),那么我们就可以建立一个“该类的比较器”来进行排序,这个“比较器”只需要实现Comparator接口即可。也就是说,我们可以通过实现Comparator来新建一个比较器,然后通过这个比较器对类进行排序。
注意:
1、若一个类要实现Comparator接口:它一定要实现compare(T o1, T o2) 函数,但可以不实现 equals(Object obj) 函数。
2、int compare(T o1, T o2) 是“比较o1和o2的大小”。返回“负数”,意味着“o1比o2小”;返回“零”,意味着“o1等于o2”;返回“正数”,意味着“o1大于o2”。
现在假如上面的Person类没有实现Comparable接口,该如何比较大小呢?我们可以新建一个类,让其实现Comparator接口,从而构造一个“比较器"。
public class PersonCompartor implements Comparator<Person>
{
@Override
public int compare(Person o1, Person o2)
{
return o1.getAge()-o2.getAge();
}
}
public class Person
{
String name;
int age;
public Person(String name, int age)
{
super();
this.name = name;
this.age = age;
}
public String getName()
{
return name;
}
public int getAge()
{
return age;
}
public static void main(String[] args)
{
Person[] people=new Person[]{new Person("xujian", 20),new Person("xiewei", 10)};
System.out.println("排序前");
for (Person person : people)
{
System.out.print(person.getName()+":"+person.getAge());
}
Arrays.sort(people,new PersonCompartor());
System.out.println("\n排序后");
for (Person person : people)
{
System.out.print(person.getName()+":"+person.getAge());
}
}
}
2.4.3. Comparable和Comparator区别比较
Comparable是排序接口,若一个类实现了Comparable接口,就意味着“该类支持排序”。而Comparator是比较器,我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。
Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。
两种方法各有优劣, 用Comparable 简单, 只要实现Comparable 接口的对象直接就成为一个可以比较的对象,但是需要修改源代码。
用Comparator 的好处是不需要修改源代码, 而是另外实现一个比较器, 当某个自定义的对象需要作比较的时候,把比较器和对象一起传递过去就可以比大小了, 并且在Comparator 里面用户可以自己实现复杂的可以通用的逻辑,使其可以匹配一些比较简单的对象,那样就可以节省很多重复劳动了。
3. 数据类型
3.1. float f=3.4;是否正确?
false。精度不准确,应该用强制类型转换,如下所示:float f=(float)3.4 或float f = 3.4f
在java里面,没小数点的默认是int,有小数点的默认是 double;
3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F;。
3.2. 3*0.1的结果是?
0.30000000000000004
3*0.1 == 0.3 将会返回什么?true 还是 false?
false,因为有些浮点数不能完全精确的表示出来。 浮点数精度默认为6位
3.3. 关于String类型的说明
- String不是基本类型。
- String是final的
- StringBuffer是非线程安全的。因此效率高
- StringBuilder是线程安全的。
3.4. char型变量中能不能存贮一个中文汉字?为什么?
char是16位的,占两个字节
汉字通常使用GBK或者UNICODE编码,也是使用两个字节
所以可以存放汉字
3.5. 什么是比特(Bit),什么是字节(Byte),什么是字符(Char),它们长度是多少,各有什么区别(一般是笔试题的选择题里面出的多一点)
Bit是最小的传输单位,byte是最小的存储单位,1byte=8bit,char 是一种基本数据类型,1char=2byte.
3.6. swtich是否能作用在byte上,是否能作用在long上,是否能作用在String上?
在java1.7之前,Java 虚拟机和字节代码这个层次上,只支持在 switch 语句中使用与整数类型兼容的类型,也就是byte,short,int,char以及Enum(枚举) 。但是不能作用在long和String上面
java1.7之后,switch可以作用在 String上。具体实现参看:
java7新特性之switch字符串比较原理
java基础(六) switch语句的深入解析
注:switch作用在String上从JDK1.7开始支持,实质是编译时将字符串替换为了其对应的hash值
4. Java反射
4.1. 相关问题
关于反射的相关定义和问题
4.1.1. 什么是反射?等同于映射吗?
完全不相关的。反射是一个机制,映射是一种关系。
反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取类的信息以及动态调用对象的方法的功能称为java语言的反射机制。
映射是一种对应关系,在很多的情况下,表示一种存在的联系而已。
4.1.2. 反射能做什么
- 在运行时判断任意一个对象所属的类;
- 在运行时构造任意一个类的对象;
- 在运行时判断任意一个类所具有的成员变量和方法;
- 在运行时调用任意一个对象的方法;
- 生成动态代理。
4.2. jdk中相关类、方法、参数等
4.2.1. Class
-
getField(String name) :返回一个 Field 对象,它反映此 Class 对象所表示的类或接口的指定公共成员字段。
-
getFields() :返回一个包含某些 Field 对象的数组,这些对象反映此 Class 对象所表示的类或接口的所有可访问公共字段。
-
getDeclaredField(String name) :返回一个 Field 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明字段。
-
getDeclaredFields() :返回 Field 对象的一个数组,这些对象反映此 Class 对象所表示的类或接口所声明的所有字段,包括公共、保护、默认(包)访问和私有字段,但不包括继承的字段。
-
getMethod(String name, Class<?>… parameterTypes) :它反映此 Class 对象所表示的类或接口的指定公共成员方法。
- name 参数是一个 String,用于指定所需方法的简称。
- parameterTypes 参数是按声明顺序标识该方法形参类型的 Class 对象的一个数组。如果 parameterTypes 为 null,则按空数组处理。
-
getMethods() :返回一个包含某些 Method 对象的数组,这些对象反映此 Class 对象所表示的类或接口**(包括那些由该类或接口声明的以及从超类和超接口继承的那些的类或接口)的公共 member 方法**。数组类返回从 Object 类继承的所有(公共)member 方法。返回数组中的元素没有排序,也没有任何特定的顺序。如果此 Class 对象表示没有公共成员方法的类或接口,或者表示一个基本类型或 void,则此方法返回长度为 0 的数组
-
getDeclaredMethods(),该方法是获取本类中的所有方法,包括私有的(private、protected、默认以及public)的方法。
-
getDeclaredMethod(String name,Class<?>… parameterTypes):返回一个 Method 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法
- name 参数是一个 String,它指定所需方法的简称
- parameterTypes 参数是 Class 对象的一个数组,它按声明顺序标识该方法的形参类型
-
getSuperclass() :返回表示此 Class 所表示的实体(类、接口、基本类型或 void)的超类的 Class。
-
newInstance() :创建此 Class 对象所表示的类的一个新实例。
-
getTypeParameters():按声明顺序返回 TypeVariable 对象的一个数组,这些对象表示用此 GenericDeclaration 对象所表示的常规声明来声明的类型变量。如果底层常规声明不声明类型变量,则返回长度为 0 的数组。
4.2.2. Type
Type 是 Java 编程语言中所有类型的公共高级接口。它们包括原始类型、参数化类型、数组类型、类型变量和基本类型。
4.2.2.1. ParameterizedType
public interface ParameterizedTypeextends Type
ParameterizedType 表示参数化类型,如 Collection。 这个接口貌似和泛型有关,但还不是很清楚,标记一下
- getActualTypeArguments():返回表示此类型实际类型参数的 Type 对象的数组。
注意,在某些情况下,返回的数组为空。如果此类型表示嵌套在参数化类型中的非参数化类型,则会发生这种情况。
测试代码
public static void main(String[] args) {
O cpaf = new O();
//getFields只能获取可访问公共字段
Field[] fs = O.class.getFields();
System.out.println(fs.length);//0
//所有字段,包括公共、保护、默认(包)访问和私有字段,但*不包括继承的字段
Field[] dfs = cpaf.getClass().getDeclaredFields();
System.out.println(dfs.length);//2
for (Field field : dfs) {
System.out.println(field);
}
//(包括那些由该类或接口声明的以及从超类和超接口继承的那些的类或接口)的*公共 member 方法
Method[] ms = cpaf.getClass().getMethods();
System.out.println(ms.length);//13,因为还有wait、equals等超类和接口的方法
for (Method method : ms) {
System.out.println(method);
}
//本类中的所有方法,包括私有的(private、protected、默认以及public)的方法
Method[] dms = cpaf.getClass().getDeclaredMethods();
System.out.println(dms.length);//4
for (Method method : dms) {
System.out.println(method);
}
System.out.println(O.class.getSuperclass());//class java.lang.Object
try {
O o1 = O.class.newInstance();
//下边的例子可以看出getType和getGenericType的差别
Field agef = o1.getClass().getDeclaredField("age");
Type t = agef.getGenericType();
System.out.println(t);//int
Class c = agef.getType();
System.out.println(c);//int
Field listf = o1.getClass().getDeclaredField("list");
Type listt = listf.getGenericType();
System.out.println(listt);//java.util.List<java.lang.String>
Class listc = listf.getType();
System.out.println(listc);//interface java.util.List
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
O的结构
package test;
import java.util.List;
class T extends Thread{
}
public class O {
private String name;
private int age;
private List<String> list;
private List<T> ts;
public List<String> getList() {
return list;
}
public void setList(List<String> list) {
this.list = list;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
下边是项目中用到的关于Type的代码:
private Object r(String jsonStr,MethodParameter methodParameter,
String paramName,WebDataBinder binder,Class clazz) throws Exception {
if (StringUtils.isEmpty(jsonStr)) {
return null;
}
JSONObject obj = JSONObject.fromObject(jsonStr);
字符串,直接返回
if (String.class == clazz) {
return obj.get(paramName);
}
//List,转换成JSONArray
if (List.class == clazz) {
JSONArray array = (JSONArray) obj.get(paramName);
if (array.size() < 1) {
return null;
}
Type genericType = methodParameter.getGenericParameterType();
if (genericType instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) genericType;
//得到泛型里的class类型对象
Class<?> genericClazz = (Class<?>) pt.getActualTypeArguments()[0];
List list = new ArrayList();
for (int i = 0, size = array.size(); i < size; i++) {
list.add(getEntity(genericClazz, array.getJSONObject(i), binder, methodParameter));
}
return list;
}
throw new Exception("暂时没有处理的类型");
}
//目前其他类型暂时按照POJO处理,如果还有,再扩展
JSONObject paramObj = (JSONObject) obj.get(paramName);
return getEntity(clazz, paramObj, binder, methodParameter);
}
4.2.3. Field
Field 提供有关类或接口的单个字段的信息,以及对它的动态访问权限。反射的字段可能是一个类(静态)字段或实例字段。
- getType():返回一个 Class 对象,它标识了此 Field 对象所表示字段的声明类型。
- getGenericType():返回一个 Type 对象,它表示此 Field 对象所表示字段的声明类型。
- getModifiers():以整数形式返回由此 Field 对象表示的字段的 Java 语言修饰符。应该使用
Modifier
类对这些修饰符进行解码。 - getName():返回此 Field 对象表示的字段的名称。
- setAccessible(boolean flag):将此对象的 accessible 标志设置为指示的布尔值。值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。值为 false 则指示反射的对象应该实施 Java 语言访问检查。 如果 flag 为 true,并且不能更改此对象的可访问性(例如,如果此元素对象是 Class 类的 Constructor 对象),则会引发 SecurityException
- isAccessible():获取此对象的 accessible 标志的值。
其中,该修饰符是java.lang.reflect.Modifier的静态属性。
4.2.3.1. getType() 和 getGenericType()的区别 :
- 首先是返回的类型不一样,一个是Class对象,一个是Type接口。
- 如果属性是一个泛型,从getType()只能得到这个属性的接口类型。但从getGenericType()还能得到这个泛型的参数类型。
- getGenericType()如果当前属性有签名属性类型就返回,否则就返回 Field.getType()。
- 我的理解是getGenericType(),针对的是List<>、Set<>中泛型的类型,也就是第二点提到的
4.2.4. Modifier
Modifier说明参见jdk 1.6 api
对应表如下:
PUBLIC: 1
PRIVATE: 2
PROTECTED: 4
STATIC: 8
FINAL: 16
SYNCHRONIZED: 32
VOLATILE: 64
TRANSIENT: 128
NATIVE: 256
INTERFACE: 512
ABSTRACT: 1024
STRICT: 2048
4.3. 示例
- java通过反射获取List中的泛型
这个起源自网络上的一个示例
Field[] fields = bean.getClass().getDeclaredFields();
for(Field f : fields){
f.setAccessible(true);
if(f.getType() == java.util.List.class){
// 如果是List类型,得到其Generic的类型
Type genericType = f.getGenericType();
if(genericType == null) continue;
// 如果是泛型参数的类型
if(genericType instanceof ParameterizedType){
ParameterizedType pt = (ParameterizedType) genericType;
//得到泛型里的class类型对象
Class<?> genericClazz = (Class<?>)pt.getActualTypeArguments()[0];
}
}
}
- 项目中用到的,执行某对象的某方法
class Back{
private void t(Object object,String methodStr,Object[] args){
try{
Class[] classes = new Class[args.length];
for (int i = 0 ;i<args.length;i++) {
Class c = args[i].getClass();
classes[i] = c == TsRequest.class ? HttpServletRequest.class : c;
}
//因为controller都被spring包装过了,所以使用jdk本身方法可能无效?
Class clazz = AopUtils.getTargetClass(object);
Method method = clazz.getDeclaredMethod(methodStr,classes);
List<Map<String, String>> list = (List<Map<String, String>>) method.invoke(object,args);
if (!CollectionUtils.isEmpty(list)) {
listBacklog.addAll(list);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
Back back = new Back();
//因为每个角色都有菜单,所以这里暂时不做空指针处理。
for (SysMenu menu : sysMenuMapper.getMenuByRoleId(role.getRoleId())) {
if (StringUtils.isEmpty(menu.getUrl())) {
continue;
}
String url = menu.getUrl();
//这里以及下边使用Back方法时注意按照参数顺序写数组元素
Object[] args = new Object[]{request, menu};
// 注师基本信息变更审批
if (url.startsWith(Constans.CPAMS_BACKLOG_URL_11)) {
back.t(cpaAcctBchgController, "getCpaAcctBchgListForBacklog", args);
}
5. 变量
volatile
5.0.1. Java 中的编译期常量是什么?使用它又什么风险?
公共静态不可变(public static final )变量也就是我们所说的编译期常量,这里的 public 可选的。
实际上这些变量在编译时会被替换掉,因为编译器知道这些变量的值,并且知道这些变量在运行时不能改变。这种方式存在的一个问题是你使用了一个内部的或第三方库中的公有编译时常量,但是这个值后面被其他人改变了,但是你的客户端仍然在使用老的值,甚至你已经部署了一个新的jar。为了避免这种情况,当你在更新依赖 JAR 文件时,确保重新编译你的程序。
6. 注解
使用注解最主要的部分在于对注解的处理,那么就会涉及到注解处理器。
从原理上讲,注解处理器就是通过反射机制获取被检查方法上的注解信息,然后根据注解元素的值进行特定的处理。
注解的可用的类型包括以下几种:所有基本类型、String、Class、enum、Annotation、以上类型的数组形式。
元素不能有不确定的值,即要么有默认值,要么在使用注解的时候提供元素的值。
而且元素不能使用null作为默认值。
注解在只有一个元素且该元素的名称是value的情况下,在使用注解的时候可以省略“value=”,直接写需要的值即可。
注解的语法比较简单。Java内置了一些注解
- Override,表示当前的方法定义将覆盖超类中的方法。
- Deprecated,使用了注解为它的元素编译器将发出警告,因为注解@Deprecated是不赞成使用的代码,被弃用的代码。
- SuppressWarnings,关闭不当编译器警告信息。
Java还提供了4中注解,专门负责新注解的创建。
6.1. @Target
表示该注解可以用于什么地方,可能的ElementType参数有:
- CONSTRUCTOR:构造器的声明
- FIELD:域声明(包括enum实例)
- LOCAL_VARIABLE:局部变量声明
- METHOD:方法声明
- PACKAGE:包声明
- PARAMETER:参数声明
- TYPE:类、接口(包括注解类型)或enum声明
6.2. @Retention
表示需要在什么级别保存该注解信息。
可选的RetentionPolicy参数包括:
- SOURCE:注解将被编译器丢弃
- CLASS:注解在class文件中可用,但会被VM丢弃
- RUNTIME:VM将在运行期间保留注解,因此可以通过反射机制读取注解的信息。
6.3. @Document
将注解包含在Javadoc中
6.4. @Inherited
允许子类继承父类中的注解
import java.lang.annotation.*;
/**
* 用来表示Api*Controller中参数是否需要特殊处理
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JsonObject {}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
7. 泛型与枚举
泛型和枚举都是JDK1.5版本之后加入的新特性,泛型将程序代码的类型检查提前到了编译期间进行,枚举类型增强了程序代码的健壮性。
7.1. 枚举
枚举是一个类类型,是JDK1.5的新特性。枚举的关键字是enum。Java中所有的枚举类都是java.lang.Enum的子类
注意:枚举类中可以包含成员有【字段(常量)、方法(构造方法、普通方法)】
7.1.1. 枚举成员(常量)
// 枚举定义
public enum Color {
RED, GREEN, BLANK, YELLOW
}
// 枚举值的使用
static boolean isRed( Color color ){
if ( Color.RED.equals( color )) {
return true ;
}
return false ;
}
// 枚举使用
public static void main(String[] args) {
System.out.println( isRed( Color.BLANK ) ) ; //结果: false
System.out.println( isRed( Color.RED ) ) ; //结果: true
}
7.1.2. Switch
JDK1.5之后的switch语句支持Byte,short,int,char,enum类型,使用枚举,能让我们的代码可读性更强,JDK1.7之后开始支持String类型
7.1.3. 枚举方法
要自定义方法,必须在enum实例序列的最后添加一个分号。而且 Java 要求必须先定义 enum实例
public enum Color {
//因为enum的构造方法是私有的,因此,这里相当于new出来多个enum常量
RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);
// 成员变量
private String name;
private int index;
// 构造方法:注意**private**
private Color(String name, int index) {
this.name = name;
this.index = index;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
//覆盖方法
@Override
public String toString() {
return this.index+"_"+this.name;
}
// 普通方法
public static void main(String[] args) {
// 输出某一枚举的值
System.out.println(Color.RED.getName());
System.out.println(Color.RED.getIndex());
// 遍历所有的枚举,enum默认有一个values方法
for (Color color : Color.values()) {
System.out.println(color + " name: " + color.getName() + " index: " + color.getIndex());
}
}
}
红色
1
RED name: 红色 index: 1
GREEN name: 绿色 index: 2
BLANK name: 白色 index: 3
YELLO name: 黄色 index: 4
7.1.4. 覆盖枚举的方法
例如上例中覆盖toString()
7.1.5. 实现接口
所有的枚举都继承自java.lang.Enum类。由于Java 不支持多继承,所以枚举对象不能再继承其他类,例:
interface Behaviour {
void print();
String getInfo();
}
public enum Color implements Behaviour {
RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);
// 成员变量
private String name;
private int index;
// 构造方法
private Color(String name, int index) {
this.name = name;
this.index = index;
}
// 接口方法
@Override
public String getInfo() {
return this.name;
}
@Override
public void print() {
System.out.println(this.index + ":" + this.name);
}
}
7.1.6. 使用接口组织枚举
public interface Food {
enum Coffee implements Food {
BLACK_COFFEE, DECAF_COFFEE, LATTE, CAPPUCCINO
}
enum Dessert implements Food {
FRUIT, CAKE, GELATO
}
}
7.1.7. 枚举集合
java.util.EnumSet和java.util.EnumMap是两个枚举集合。EnumSet保证集合中的元素不重复;EnumMap中的key是enum类型,而value则可以是任意类型。
7.1.8. 枚举和普通类的区别与联系
- 枚举与类都可以实现多接口;访问控制符都可以使用(4p),但枚举中默认的是private,类中默认的是package;
- 枚举直接继承java.lang.Enum类,普通类是继承java.lang.Object;其中java.long.Enum类实现了java.long.Serializable和java.long.Comparable两个接口。
- 使用enum定义、非抽象的枚举默认修饰符为public final,因此枚举不能派生子类。
- 枚举的构造器只能使用private访问控制符,如果省略了枚举的访问修饰符其默认为private修饰;因为枚举的字段不能初始化,对象类型的必须调用构造方法,所有有多少个成员构造方法就会运行多少次;
- 枚举的所有实例必须在枚举的第一行显示列出,否则这个枚举永远都不能生产实例,列出这些实例时系统会自动添加public static final修饰,无需程序员显式添加
- 所有的枚举类都提供了一个values方法,该方法可以很方便的遍历所有的枚举值
- 关键字:枚举是enum,类是class
- 枚举是类类型,类是引用类型
7.2. 泛型
泛型(Generic)的本质是类型参数化,也就是说所操作的数据类型被指定为一个参数。泛型将程序代码的类型检查提前到了编译期间(只是允许程序员在编译时检测到非法的类型而已,但是在运行期时,其中的泛型标志会变化为 Object 类型。)
7.2.1. 泛型方法
你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
下面是定义泛型方法的规则:
- 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的)。
- 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
- 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
- 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char的等)。
public class GenericTest {
public static <E, T, K> E ret(T param1,K p2){
E p = (E)param1;
return p;
}
//类型参数声明部分<T, E>(T和E代表的是两种不同的类型),在方法返回类型void之前
public static <T, E> void printArray(E[] inputArray) {
for (E element : inputArray) {
System.out.printf("%s ", element);
}
}
public static void main(String[] args) {
System.out.println(ret(1,""));
System.out.println(ret("str",""));
System.out.println(ret(new GenericTest(),""));
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3, 4, 5 };
Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };
System.out.println("整型数组元素为:");
printArray(intArray); // 传递一个整型数组
System.out.println("\n双精度型数组元素为:");
printArray(doubleArray); // 传递一个双精度型数组
System.out.println("\n字符型数组元素为:");
printArray(charArray); // 传递一个字符型数组
}
}
7.2.1.1. 有界的类型参数:
可能有时候,你会想限制那些被允许传递到一个类型参数的类型种类范围&。例如,一个操作数字的方法可能只希望接受Number或者Number子类的实例。这就是有界类型参数的目的。
要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界。
<? extends T>和<? super T>的区别
- <? extends T>表示该通配符所代表的类型是T类型的子类。
- <? super T>表示该通配符所代表的类型是T类型的父类。
public class GenericTest {
public static <T extends Integer, K extends Double> double max(T p1,K p2){
//T p = (T)p2;编译错误:不能cast K to T
return p2 > p1 ? p2 : p1;
}
public static void main(String[] args) {
System.out.println(max(5,3D));
}
}
7.2.2. 泛型类
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。
和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
public class GenericClass<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
GenericClass<Integer> integer = new GenericClass<Integer>();
GenericClass<String> string = new GenericClass<String>();
integer.add(new Integer(10));
string.add(new String("菜鸟教程"));
System.out.printf("整型值为 :%d\n\n", integer.get());
System.out.printf("字符串为 :%s\n", string.get());
}
}
7.2.3. 泛型接口的定义和使用
如List、Set等接口
7.3. 类型通配符
1、类型通配符一般是使用?代替具体的类型参数。例如 List<?> 在逻辑上是List,List 等所有List<具体类型实参>的父类。
2、类型通配符上限通过形如List<? extends Number>来定义,如此定义就是通配符泛型值接受Number及其下层子类类型。
3、类型通配符下限通过形如 List<? super Number>来定义,表示类型只能接受Number及其三层父类类型,如Objec类型的实例。
import java.util.*;
public class GenericTest {
public static void main(String[] args) {
List<String> name = new ArrayList<String>();
List<Integer> age = new ArrayList<Integer>();
List<Number> number = new ArrayList<Number>();
name.add("icon");
age.add(18);
number.add(314);
// getUperNumber(name);//1错误,因为getUperNumber()方法中的参数已经限定了参数泛型上限为Number,所以泛型为String是不在这个范围之内,所以会报错
getUperNumber(age);// 2
getUperNumber(number);// 3
}
public static void getData(List<?> data) {
System.out.println("data :" + data.get(0));
}
public static void getUperNumber(List<? extends Number> data) {
System.out.println("data :" + data.get(0));
}
}
8. 类与接口
8.1. 基本类
8.2. 一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制?
可以包括多个类,但是只能出现一个public修饰的类,但是可以出现多个非public修饰的类,这些类编译后,为独立的class文件。
8.3. 内部类
参考:Java内部类详解
匿名内部类 :匿名内部类则被编译为类名+$+数字.class
可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。下面就先来了解一下这四种内部类的用法。
8.3.1. 成员内部类
成员内部类是最普通的内部类,它的定义为位于另一个类的内部
如下,类P像是类Test的一个成员,Test称为外部类。成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。
public class Test {
private int count = 0;
class P {
public void out() {
System.out.println(count);
}
}
}
不过要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:
外部类.this.成员变量
外部类.this.成员方法
虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问:
public class Test {
private int count = 0;
P p = new P();
public void outter() {
//外部类想要访问内部类成员,需要先对象化
System.out.println(p.pCount);
}
class P {
//内部类可以无条件访问外部类的成员
public int pCount = 1;
public void inner() {
System.out.println(count);
}
}
}
成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。创建成员内部类对象的一般方式如下:
public class Test {
public static void main(String[] args) {
// 第一种方式:
Outter outter = new Outter();
Outter.Inner inner = outter.new Inner(); // 必须通过Outter对象来创建,使用new Outter.Inner()报错,因为Inner很明显不是Outter的成员
// 第二种方式:这只是一种变种而已,让外部类持有内部类实例供外部访问
Outter.Inner inner1 = outter.getInnerInstance();
}
}
class Outter {
private Inner inner = null;
public Outter() {}
public Inner getInnerInstance() {
if (inner == null)
inner = new Inner();
return inner;
}
class Inner {
public Inner() {}
}
}
内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。比如上面的例子,如果成员内部类Inner用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。
这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。
8.3.2. 局部内部类
局部内部类是 定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。
注意,局部内部类就像是方法里面的一个局部变量一样,是 不能有public、protected、private以及static修饰符的。
class People {
public People() {}
}
class Man {
public Man() {}
public People getWoman() {
class Woman extends People { // 局部内部类
int age = 0;
}
return new Woman();
}
}
8.3.3. 匿名内部类
匿名内部类应该是平时我们编写代码时用得最多的,在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。
scan_bt.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
}
});
history_bt.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
}
});
使用匿名内部类能够在实现父类或者接口中的方法情况下同时产生一个相应的对象,但是前提是这个父类或者接口必须先存在才能这样使用。当然像下面这种写法也是可以的,跟上面使用匿名内部类达到效果相同。
private void setListener()
{
scan_bt.setOnClickListener(new Listener1());
history_bt.setOnClickListener(new Listener2());
}
class Listener1 implements View.OnClickListener{
@Override
public void onClick(View v) {}
}
class Listener2 implements View.OnClickListener{
@Override
public void onClick(View v) {}
}
这种写法虽然能达到一样的效果,但是既冗长又难以维护,所以一般使用匿名内部类的方法来编写事件监听代码。同样的,匿名内部类也是不能有访问修饰符和static修饰符的。
匿名内部类是唯一一种没有构造器的类。正因为其没有构造器,所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调。匿名内部类在编译的时候由系统自动起名为Outter$1.class。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。
8.3.4. 静态内部类
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。
public class Test {
public static void main(String[] args) {
Outter.Inner inner = new Outter.Inner();
}
}
class Outter {
public Outter() {
}
static class Inner {
public Inner() {
}
}
}
8.3.5. 为什么成员内部类可以无条件访问外部类的成员?
译器在进行编译的时候,会将成员内部类单独编译成一个字节码文件,文件名称为·外部类$内部类.class·
8.4. 抽象类
1 使用abstract
修饰的class
2 如果一个class
中包含abstract
方法,那么这个类必须为抽象类;
3 因为抽象类(可能)包含抽象方法,所以抽象类不能被实例化;
4 抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public。
5 如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为为abstract类。
抽象类是子类通用特性的抽象,不能被实例化,只能用作超类。除了不能实例化之外,和普通的Java类没有区别。
8.5. 接口
接口是一种“契约模式”
1 接口中 可以含有变量和方法。
2 接口中的 变量会被隐式地指定为public static final变量(并且只能是public static final变量,用private修饰会报编译错误)
3 而 方法会被隐式地指定为public abstract方法且只能是public abstract方法(用其他关键字,比如private、protected、static、 final等修饰会报编译错误),并且接口中所有的方法不能有具体的实现,也就是说,接口中的方法必须都是抽象方法。从这里可以隐约看出接口和抽象类的区别,接口是一种极度抽象的类型,它比抽象类更加“抽象”,并且一般情况下不在接口中定义变量。
8.5.1. jdk中一些重要的接口:
- Serializable:序列化接口
- Cloneable:该对象能被克隆,能使用Object.clone()方法。在没有实现Cloneable的类对象上使用clone(),会抛出异常
- Comparable:内比较器。实现compareTo(o1)方法,使元素可以排序
- Comparator:外比较器。实现compare(o1,o2)方法
- InvocationHandler:动态代理实现接口。实现invoke(classloader,interfaces,handler)实现代理
- Runnable:线程。实现run方法
- Callable:多线性。实现call方法,和FutureTask配合使用
- Collection、List、Set、Map:集合。
8.5.1.1. Cloneable
Cloneable和Serializable一样都是 标记型接口,它们内部都没有方法和属性,implements Cloneable表示该对象能被克隆,能使用Object.clone()方法。如果没有implements Cloneable的类调用Object.clone()方法就会抛出CloneNotSupportedException。
克隆的分类
- 浅克隆(shallow clone),浅拷贝是指拷贝对象时仅仅拷贝对象本身和对象中的基本变量,而不拷贝对象包含的引用指向的对象。
- 深克隆(deep clone),深拷贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。
要让一个对象进行克隆,其实就是两个步骤:
- 让该类实现java.lang.Cloneable接口;
- 重写(override)Object类的clone()方法。
8.6. 抽象类和接口的区别
继承是一个 "是不是(is a)"的关系,而 接口 实现则是 "有没有(has a)"的关系
抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类Airplane,将鸟设计为一个类Bird,但是不能将 飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将 飞行 设计为一个接口Fly,包含方法fly( ),然后Airplane和Bird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承Airplane即可,对于鸟也是类似的,不同种类的鸟直接继承Bird类即可。
abstract的method是否可同时是static,是否可同时是synchronized?都不可以
参数 | 抽象类 | 接口 |
---|---|---|
默认的方法实现 | 它可以有默认的方法实现 接口完全是抽象的。 | 它根本不存在方法的实现 |
实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现 |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
与正常Java类的区别 | 除了你不能实例化抽象类之外,它和普通Java类没有任何区别 | 接口是完全不同的类型 |
访问修饰符 | 抽象方法可以有public、protected和default这些修饰符 | 接口方法默认修饰符是public。你不可以使用其它修饰符。 |
main方法 | 抽象方法可以有main方法并且我们可以运行它 | 接口没有main方法,因此我们不能运行它。 |
多继承 | 抽象方法可以继承一个类和实现多个接口 | 接口只可以继承一个或多个其它接口 |
速度 | 它比接口速度要快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。 |
添加新方法 | 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。 | 如果你往接口中添加方法,那么你必须改变实现该接口的类。 |
8.7. 抽象类和接口的使用场景
如果 子类需要一些默认的实现,那么必须使用抽象类
如果 需要实现多重继承,必须使用接口
9. Overload和Override的区别,即重载和重写的区别。
方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是 父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。
- (1)方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
- (2)方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
- (3)方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
9.1. 重写(Override)
重写是 子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!
重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。例如: 父类的一个方法申明了一个检查异常 IOException,但是在重写这个方法的时候不能抛出 Exception 异常,因为 Exception 是 IOException 的父类,只能抛出 IOException 的子类异常。
访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected。
构造方法不能被重写。
当需要在子类中调用父类的被重写方法时,要使用super关键字。
9.2. 重载(Overload)
重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。
每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
最常用的地方就是构造器的重载。
重载规则:
- 被重载的方法必须改变参数列表(参数个数或类型不一样);
- 被重载的方法可以改变返回类型;
- 被重载的方法可以改变访问修饰符;
- 被重载的方法可以声明新的或更广的检查异常;
- 方法能够在同一个类中或者在一个子类中被重载。
- 无法以返回值类型作为重载函数的区分标准。
10. 集合
10.1. Java集合框架的基础接口有哪些?
- Collection为集合层级的根接口。一个集合代表一组对象,这些对象即为它的元素。Java平台不提供这个接口任何直接的实现。
- Set是一个不能包含重复元素的集合。这个接口对数学集合抽象进行建模,被用来代表集合,就如一副牌。
- List是一个有序集合,可以包含重复元素。你可以通过它的索引来访问任何元素。List更像长度动态变换的数组。
- Map是一个将key映射到value的对象.一个Map不能包含重复的key:每个key最多只能映射一个value。
- 一些其它的接口有Queue、Dequeue、SortedSet、SortedMap和ListIterator。
10.2. 当一个集合被作为參数传递给一个函数时,怎样才干够确保函数不能改动它?
在作为參数传递之前,我们能够使用Collections.unmodifiableCollection(Collection c)方法创建一个仅仅读集合,这将确保改变集合的不论什么操作都会抛出UnsupportedOperationException。
10.3. Collections
常用api
- static void sort(List list, Comparator<? super T> c) :根据指定比较器产生的顺序对指定列表进行排序。
- static Collection synchronizedCollection(Collection c):返回指定 collection 支持的同步(线程安全的)collection。
- static List synchronizedList(List list):返回指定列表支持的同步(线程安全的)列表。
- static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) :返回由指定映射支持的同步(线程安全的)映射。
- static Set synchronizedSet(Set s) 返回指定 set 支持的同步(线程安全的)set。
- 还有其它的synchronized方法,这里不一一列举了…
- static Collection unmodifiableCollection(Collection<? extends T> c):返回指定 collection 的不可修改视图。
- 还有其它的unmodifiable方法,这里不一一列举了…
10.4. Map
Map的迭代方法有两种
- 使用keySet()
- 使用entrySet()
- 使用keySet迭代,需要从Map中再次获取value;而使用entryKey(),则不需要再次从Map中获取value。如果迭代的过程中需要使用value,用entrySet迭代比较好
当我们需要一个同步的HashMap时,有两种选择:
- 使用Collections.synchronizedMap(…)来同步HashMap。
- 使用ConcurrentHashMap
Map m = Collections.synchronizedMap(new HashMap());
...
Set s = m.keySet(); // Needn't be in synchronized block
...
synchronized(m) { // **Synchronizing on m, not s!**
Iterator i = s.iterator(); // **Must be in synchronized block**
while (i.hasNext())
foo(i.next());
}
这两个选项之间的首选是使用ConcurrentHashMap,这是因为我们不需要锁定整个对象,以及通过ConcurrentHashMap分区地图来获得锁。
10.4.1. HashMap
HashMap是最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。因为键对象不可以重复,所以HashMap最多只允许一条记录的键为Null,允许多条记录的值为Null,是非同步的
10.4.2. Hashtable
Hashtable与HashMap类似,是HashMap的线程安全版,它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了Hashtale在写入时会比较慢,它继承自Dictionary类,不同的是它不允许记录的键或者值为null,同时效率较低。
10.4.3. ConcurrentHashMap
线程安全,并且锁分离。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hash table,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。
10.4.4. LinkedHashMap
LinkedHashMap 保存了记录的插入顺序,在用Iteraor遍历LinkedHashMap时,先得到的记录肯定是先插入的,在遍历的时候会比HashMap慢,有HashMap的全部特性。
10.4.5. TreeMap
TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序(自然顺序),也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。不允许key值为空,非同步的;
10.4.6. HashMap和Hashtable的区别
一般现在不建议用HashTable,
①是HashTable是遗留类,内部实现很多没优化和冗余。
②即使在多线程环境下,现在也有同步的ConcurrentHashMap替代,没有必要因为是多线程而用HashTable。
- HashMap的键和值都允许有null值存在,而HashTable则不行。
- HashMap是非线程安全的,HashTable是线程安全的。
- 因为线程安全的问题,HashMap效率比HashTable的要高。
- Hashtable是同步的,而HashMap不是。因此,HashMap更适合于单线程环境,而Hashtable适合于多线程环境。
10.4.7. HashMap与TreeMap
- HashMap通过hashcode对其内容进行快速查找,而TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。
- 在Map 中插入、删除和定位元素,HashMap是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。使用HashMap要求添加的键类明确定义了hashCode()和 equals()的实现。
两个map中的元素一样,但顺序不一样,导致hashCode()不一样。
同样做测试:
在HashMap中,同样的值的map,顺序不同,equals时,false;
而在treeMap中,同样的值的map,顺序不同,equals时,true,说明,treeMap在equals()时是整理了顺序了的。
HashMap<String, String> map = new HashMap<>();
map.put(null, null);
10.5. Collection
10.5.1. Collection和Collections的区别
- Collection是单值存放的最大父接口,是集合类的上级接口,继承自它的接口有List、Set、Queue和SortedSet。
- Collections是针对集合类的一个帮助类,它提供了一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。
10.5.2. List, Set, Map是否继承自Collection接口?
List 和 Set 继承了Collection接口
但是Map和Collection之间没有继承关系,因为一个是键值对容器,一个是单值容器,无法兼容
10.5.3. 快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?
- 快速失败:当你在迭代一个集合的时候,如果有另一个线程正在修改你正在访问的那个集合时,就会抛出一个ConcurrentModification异常。 在java.util包下的都是快速失败。
- 安全失败:你在迭代的时候会去底层集合做一个拷贝,所以你在修改上层集合的时候是不会受影响的,不会抛出ConcurrentModification异常。 在java.util.concurrent包下的全是安全失败的。
10.5.4. ArrayList和Vector有何异同点?
- 相同点:都是基于数组结构的,因此具有数组的特性
- 不同点:Vector是线程安全的,ArrayList非线程安全。因此,ArrayList的效率会比Vector更高。
10.5.5. ArrayList和LinkedList的区别
- ArrayList是实现了动态数组的数据结构,LinkedList基于链表的数据结构。
- 对于随机访问get和set,很明显ArrayList的性能要优于LinkedList。
- 对于增加和删除操作add和remove,LinkedList占优势。
- 因为LinkedList要维护前后节点之间的关系,因此需要花费更多的内存
总结: 当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList性能比较好;当操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。
10.5.6. 什么是CopyOnWriteArrayList,它与ArrayList有何不同?
CopyOnWriteArrayList是ArrayList的一个线程安全的变体,其中所有可变操作(add、set等等)都是通过对底层数组进行一次新的复制来实现的。相比较于ArrayList它的写操作要慢一些,因为它需要实例的快照。
CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差,但是读操作因为操作的对象和写操作不是同一个对象,读之间也不需要加锁,读和写之间的同步处理只是在写完后通过一个简单的"="将引用指向新的数组对象上来,这个几乎不需要时间,这样读操作就很快很安全,适合在多线程里使用,绝对不会发生ConcurrentModificationException ,因此CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。
10.5.7. Set里的元素是不能重复的,那么用什么方法来区分重复与否呢?
以HashSet为例,判断重复的逻辑是:
- 首先看hashcode是否相同,如果不同,就是不重复的
- 如果hashcode一样,再比较equals,如果不同,就是不重复的,否则就是重复的。
10.5.8. 数组、List、Set互相转换
10.5.8.1. 数组转List
String[] staffs = new String[]{"Tom", "Bob", "Jane"};
List staffsList = Arrays.asList(staffs);
需要注意的是, Arrays.asList() 返回一个受指定数组决定的固定大小的列表。所以不能做 add 、 remove 等操作,否则会报错。
List staffsList = Arrays.asList(staffs);
staffsList.add("Mary"); // UnsupportedOperationException
staffsList.remove(0); // UnsupportedOperationException
如果想再做增删操作呢?将数组中的元素一个一个添加到列表,这样列表的长度就不固定了,可以进行增删操作。
List staffsList = new ArrayList<String>();
for(String temp: staffs){
staffsList.add(temp);
}
staffsList.add("Mary"); // ok
staffsList.remove(0); // ok
10.5.8.2. 数组转Set
String[] staffs = new String[]{"Tom", "Bob", "Jane"};
Set<String> staffsSet = new HashSet<>(Arrays.asList(staffs));
staffsSet.add("Mary"); // ok
staffsSet.remove("Tom"); // ok
10.5.8.3. List转数组
String[] staffs = new String[]{"Tom", "Bob", "Jane"};
List staffsList = Arrays.asList(staffs);
Object[] result = staffsList.toArray();
10.5.8.4. List转Set
String[] staffs = new String[]{"Tom", "Bob", "Jane"};
List staffsList = Arrays.asList(staffs);
Set result = new HashSet(staffsList);
10.5.8.5. Set转数组
String[] staffs = new String[]{"Tom", "Bob", "Jane"};
Set<String> staffsSet = new HashSet<>(Arrays.asList(staffs));
Object[] result = staffsSet.toArray();
10.5.8.6. Set转List
String[] staffs = new String[]{"Tom", "Bob", "Jane"};
Set<String> staffsSet = new HashSet<>(Arrays.asList(staffs));
List<String> result = new ArrayList<>(staffsSet);
10.6. Iterator
Iterator,所有的集合类,都实现了Iterator接口,这是一个用于遍历集合中元素的接口,主要包含以下三种方法:
- hasNext()是否还有下一个元素。
- next()返回下一个元素。
- remove()删除当前元素。
Map<String,String> params = new HashMap<String,String>();
Set<Entry<String,String>> set = params.entrySet();
Iterator<Entry<String, String>> it = params.entrySet().iterator();
while(it.hasNext()){
Entry<String,String> entry = it.next();
}
List<String> list = new ArrayList<String>();
Iterator<String> it1 = list.iterator();
//ListIterator专门用来输出List的内容
ListIterator<String> it2 = list.listIterator();
while(it2.hasNext()){
// it2.previous()
// it2.hasPrevious();
// it2.next()
}
10.6.1. Iterator和ListIterator的区别
ListIterator针对List的迭代做了一些改造,因此,当操作List时,比起Iterator功能更多
- ListIterator有add()方法,可以向List中添加对象,而Iterator不能。
- ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。
- ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。
- 都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。
注意:通过ListIterator添加的元素,在本次迭代过程中不会出现
String[] staffs = new String[] { "Tom", "Bob", "Jane" };
List staffsList = Arrays.asList(staffs);
List l = new ArrayList();
l.addAll(staffsList);
ListIterator<String> li = l.listIterator();
int i = 0;
while (li.hasNext()) {
Object o = li.next();
System.out.print(o + ",");//Tom,Bob,Jane,
li.add("" + i++);
}
System.out.println();
for (Object object : l) {
System.out.print(object + ",");//Tom,0,Bob,1,Jane,2,
}
10.6.2. Collection接口的remove()方法和Iterator接口的remove()方法区别?
①性能方面
Collection的remove方法必须首先找出要被删除的项,找到该项的位置采用的是单链表结构查询,单链表查询效率比较低,需要从集合中一个一个遍历才能找到该对象;
Iterator的remove方法结合next()方法使用,比如集合中每隔一项删除一项,Iterator的remove()效率更高
②容错方面
在使用Iterator遍历时,如果使用Collection的remove则会报异常,会出现ConcurrentModificationException,因为集合中对象的个数会改变而Iterator 内部对象的个数不会,不一致则会出现该异常
在使用Iterator遍历时,不会报错,因为iterator内部的对象个数和原来集合中对象的个数会保持一致
10.6.3. 迭代器和枚举之间的区别
如果面试官问这个问题,那么他的意图一定是让你区分Iterator不同于Enumeration的两个方面:
- Iterator允许移除从底层集合的元素。
- Iterator的方法名是标准化的。
11. 多线程
不能对同一线程对象两次调用start()方法。
Java线程具有五中基本状态
- 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
- 就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
- 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
- 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
- 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
- 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
- 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
- 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
11.1. 实现多线程的四种方法
- 继承Thread类,重写run方法(Thread类也实现了Runnable接口)。因为是继承,所以在期望多继承的场景下可能不合适
- 实现Runnable接口,实现run方法。因为是实现接口,所以不与继承冲突
- 使用Thread的匿名内部类
- 实现Callable的call方法,相比于Runnable,实现Callable可以有返回值。Callable需要搭配FutureTask来使用
- 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
- 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
- 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
future模式:并发模式的一种,可以有两种形式,即无阻塞和阻塞,分别是isDone和get。其中Future对象用来存放该线程的返回值以及状态
ExecutorService e = Executors.newFixedThreadPool(3);
//submit方法有多重参数版本,及支持callable也能够支持runnable接口类型.
Future future = e.submit(new myCallable());
future.isDone() //return true,false 无阻塞
future.get() // return 返回值,阻塞直到该线程运行结束
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadUtil {
public static void main(String[] args) {
Thread t1 = new Thread(new ThreadStyle1(),"第一方式");
t1.start();
Thread t2 = new Thread(new ThreadStyle2(),"第二方式");
t2.start();
Thread t3 = new Thread() {
@Override
public void run() {
System.out.println("实现多线程的第三种方式,其实是继承自Thread的变种!");
super.run();
}
};
t3.start();
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("实现多线程的第四种方式,其实是实现Runnable的变种!");
}
});
t4.start();
//1.执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。
ThreadStyle3 t5 = new ThreadStyle3();
FutureTask<String> result = new FutureTask<String>(t5);
new Thread(result).start();
try {
//2.接收线程运算后的结果
String sum = result.get();
//FutureTask 可用于 闭锁 类似于CountDownLatch的作用,在所有的线程没有执行完成之后这里是不会执行的
System.out.println(sum);
System.out.println(result.isDone);//true
System.out.println("------------------------------------");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
//继承自Thread,重写run
class ThreadStyle1 extends Thread{
@Override
public void run() {
System.out.println("实现多线程的第一种方法:继承Thread");
super.run();
}
}
//实现Runnable的run
class ThreadStyle2 implements Runnable{
@Override
public void run() {
System.out.println("实现多线程的第二种方法:实现接口Runnable");
}
}
//方式三:实现 Callable 接口。 相较于实现 Runnable 接口的方式,方法可以有返回值,并且可以抛出异常。
class ThreadStyle3 implements Callable<String>{
//返回类型Object就是Callable<>中指定的泛型
@Override
public String call() throws Exception {
return "实现多线程的第四种方法:实现接口Callable的call方法,和FutureTask搭配使用。比较Runnable,Callable可以有返回值";
}
}
11.2. 线程池
- 减少系统资源的开销:避免新线程的创建、销毁等繁琐过程。
- 提供系统的性能:池至少有一个以上的线程, 多线程协同工作, 可响应多个客户端请求。而且可以重复利用池里空闲的线程,免去了新线程不断地创建、销毁过程.
11.2.1. ExecutorService
Executors 类提供了用于此包中所提供的执行程序服务的 工厂方法。
线程池的创建入口 Executors,真正干活的是 ExecutorService
java.util.concurrent.Executors 工厂类可以创建四种类型的线程池,通过 Executors.newXXX 方法即可创建
可以关闭 ExecutorService,这将导致其拒绝新任务。提供两个方法来关闭 ExecutorService。shutdown() 方法在终止前允许执行以前提交的任务,而 shutdownNow() 方法阻止等待任务启动并试图停止当前正在执行的任务。在终止时,执行程序没有任务在执行,也没有任务在等待执行,并且无法提交新任务。应该关闭未使用的 ExecutorService 以允许回收其资源。
- boolean isShutdown():如果此执行程序已关闭,则返回 true。
- boolean isTerminated():如果关闭后所有任务都已完成,则返回 true。
- void shutdown():启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
- List shutdownNow():试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。
- Future submit(Callable task) : 提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。
- Future<?> submit(Runnable task):提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
- Future submit(Runnable task, T result):提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
11.2.1.1. newFixedThreadPool
创建固定大小的线程池。每次提交一个任务,就会启一个线程,直到线程池的线程数量达到线程池的上限。
- FixedThreadPool是一种容量固定的线程池;
- 阻塞队列采用LinkedBlockingQueue,它是一种无界队列;
- 由于阻塞队列是一个无界队列,因此永远不可能拒绝执行任务;
- 由于采用无界队列,实际线程数将永远维持在nThreads,因此maximumPoolSize和keepAliveTime将无效。
11.2.1.2. newCachedThreadPool
创建一个可缓存的线程池。每次提交一个任务,委派给线程池空闲的线程处理, 如果木有空闲的线程, 则直接创建新线程,任务被执行完后,当前线程加入到线程池维护。其生命周期超过一定时间会被销毁回收。
- CachedThreadPool是一种可以无限扩容的线程池;
- CachedThreadPool比较适合执行时间片比较小的任务;
- keepAliveTime为60,意味着线程空闲时间超过60s就会被杀死;
- 阻塞队列采用SynchronousQueue,这种阻塞队列没有存储空间,意味着只要有任务到来,就必须得有一个工作线程来处理,如果当前没有空闲线程,那么就再创建一个新的线程。
11.2.1.3. newSingleThreadExecutor
创建只有一个线程的线程池。问题来了, 一个线程的线程池和普通创建一个线程一样么?当然不一样.线程销毁问题。
11.2.1.4. newScheduledThreadPool
创建一个大小不受限的线程池。提供定时、周期地执行任务能力。
11.2.1.5. newSingleThreadScheduledExecutor()
创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。
11.2.1.6. 接口 ScheduledExecutorService
所有超级接口:
Executor, ExecutorService
一个 ExecutorService,可安排在给定的延迟后运行或定期执行的命令。
schedule 方法使用各种延迟创建任务,并返回一个可用于取消或检查执行的任务对象。
scheduleAtFixedRate(不管有没有完成,将执行时间算在延迟时间内) 和 scheduleWithFixedDelay(执行完成后一定时间) 方法创建并执行某些在取消前一直定期运行的任务。
方法摘要
- ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit):创建并执行在给定延迟后启用的 ScheduledFuture。
- ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit):创建并执行在给定延迟后启用的一次性操作。
- ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):创建并执行一个在给定初始延迟后首次启用的定期操作,后续操作具有给定的周期;也就是将在 initialDelay 后开始执行,然后在 initialDelay+period 后执行,接着在 initialDelay + 2 * period 后执行,依此类推。
- ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class MyThreadPool {
public static void main(String[] args) {
// 固定大小线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
executorService.shutdown();
// 缓存线程池,不固定大小,当线程池中没有空闲线程时就创建新的线程
ExecutorService executorService2 = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executorService2.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
executorService2.shutdown();
// 单一线程池,和单线程的区别在于线程销毁于创建
ExecutorService executorService3 = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
executorService3.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
executorService3.shutdown();
// 调度线程池,添加了定时任务调度等功能
// 正常调度
ExecutorService executorService4 = Executors.newScheduledThreadPool(2);
for (int i = 0; i < 5; i++) {
executorService4.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
executorService4.shutdown();
// 周期性调度
// ScheduleAtFixedRate是基于固定时间间隔进行任务调度,
// ScheduleWithFixedDelay取决于每次任务执行的时间长短,是基于不固定时间间隔进行任务调度
ScheduledExecutorService executorService5 = Executors.newScheduledThreadPool(2);
long initialDelay = 1, delay = 1;
// 应用启动1S后,每隔1S执行一次
executorService5.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}, initialDelay, delay, TimeUnit.SECONDS);
// 应用启动1S后,每隔2S执行一次
executorService5.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}, initialDelay, delay, TimeUnit.SECONDS);
}
}
11.2.2. 线程池的拒绝策略
Jdk提供了四种拒绝策略。
- CallerRunsPolicy 这种策略是说线程池在没被关闭前, 直接会去执行此任务, 否则丢弃任务。
- AbortPolicy 线程拒绝策略,简单粗暴, 直接throw exception出来了, 丢弃任务
- DiscardPolicy 跟AbortPolicy一样, 直接丢弃任务, 只不过人家不抛出exception罢了。
- DiscardOldestPolicy 在线程池没被关闭的情况下, 丢弃任务等待队列中最早的任务。然后重新尝试运行该任务。
11.3. class及关键字等
11.3.1. FutrueTask
FutureTask类实际上是同时实现了Runnable和Future接口,由此才使得其具有Future和Runnable双重特性。通过Runnable特性,可以作为Thread对象的target,而Future特性,使得其可以取得新创建线程中的call()方法的返回值。
通过FutrueTask.get()方法获取子线程call()方法的返回值时,当子线程此方法还未执行完毕,ft.get()方法会一直阻塞,直到call()方法执行完毕才能取到返回值。
11.3.2. synchronized
- synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
- 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
- 每个对象只有一个锁(lock)与之相关联。
- 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
synchronized关键字的作用域有二种:
- 某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线 程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的 synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
- 某个类的范围,
synchronized static aStaticMethod{}
防止多个线程同时访问这个类中的synchronized static
方法。它可以对类的所有对象实例起作用。
11.3.2.1. 当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法
- 这要看情况而定,如果该对象的其他方法也是有synchronized修饰的,那么其他线程就会被挡在外面。否则其他线程就可以进入其他方法。
- 其它线程照样可以同时访问相同类的 另一个对象实例中的synchronized方法
synchronized static aStaticMethod{}
防止多个线程同时访问这个类中的synchronized static
方法。它可以对类的所有对象实例起作用
11.3.2.2. synchronized 方法的缺陷
同步方法,这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法.同步方法实质是将synchronized作用于object reference。――那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造成数据混乱:(
若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。
11.3.3. ThreadLocal
下边是引用jdk api中的文字
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
例如,以下类生成对每个线程唯一的局部标识符。线程 ID 是在第一次调用 UniqueThreadIdGenerator.getCurrentThreadId() 时分配的,在后续调用中不会更改。
import java.util.concurrent.atomic.AtomicInteger;
public class UniqueThreadIdGenerator {
private static final AtomicInteger uniqueId = new AtomicInteger(0);
private static final ThreadLocal < Integer > uniqueNum =
new ThreadLocal < Integer > () {
@Override protected Integer initialValue() {
return uniqueId.getAndIncrement();
}
};
public static int getCurrentThreadId() {
return uniqueId.get();
}
}
每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
该类有以下四个方法
- T get():返回此线程局部变量的当前线程副本中的值。
- protected T initialValue():返回此线程局部变量的当前线程的“初始值”。
- void remove():移除此线程局部变量当前线程的值。
- void set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。
public class ThreadLocalTest {
ThreadLocal<Long> ids = new ThreadLocal<Long>();
ThreadLocal<String> names = new ThreadLocal<String>();
public void setInfo() {
ids.set(Thread.currentThread().getId());
names.set(Thread.currentThread().getName());
}
public Long getId() {
return ids.get();
}
public String getName() {
return names.get();
}
public static void main(String[] args) throws InterruptedException {
final ThreadLocalTest test = new ThreadLocalTest();
test.setInfo();
System.out.println(test.getId()+","+test.getName());
Thread t = new Thread(new Runnable() {
public void run() {
test.setInfo();
System.out.println(test.getId()+","+test.getName());
}
});
t.start();
t.join();
System.out.println(test.getId()+","+test.getName());
}
}
11.4. sleep() 和 wait() 有什么区别?
首先sleep和wait之间没有任何关系
sleep 是Thread类的方法,指的是当前线程暂停。
wait 是Object类的方法, 指的占用当前对象的线程临时释放对当前对象的占用,以使得其他线程有机会占用当前对象。 所以调用wait方法一定是在synchronized 中进行
11.4.1. wait和notify的关系
wait和notify这两个方法是一对,wait方法阻塞当前线程,而notify是唤醒被wait方法阻塞的线程。
wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。要执行这两个方法,有一个前提就是,当前线程必须获其对象的monitor(俗称“锁”),否则会抛出IllegalMonitorStateException异常,所以这两个方法必须在同步块代码里面调用,经典的生产者和消费者模型就是使用这两个方法实现的。
11.5. 日期
1、java.util.Date
类 Date 表示特定的瞬间,精确到毫秒。从 JDK 1.1 开始,应该使用 Calendar 类实现日期和时间字段之间转换,使用 DateFormat 类来格式化和分析日期字符串。Date 中的把日期解释为年、月、日、小时、分钟和秒值的方法已废弃。
2、java.text.DateFormat(抽象类)
DateFormat 是日期/时间格式化子类的抽象类,它以与语言无关的方式格式化并分析日期或时间。日期/时间格式化子类(如 SimpleDateFormat)允许进行格式化(也就是日期 -> 文本)、分析(文本-> 日期)和标准化。将日期表示为 Date 对象,或者表示为从 GMT(格林尼治标准时间)1970 年,1 月 1 日 00:00:00 这一刻开始的毫秒数。
3、java.text.SimpleDateFormat(DateFormat的直接子类)
SimpleDateFormat 是一个以与语言环境相关的方式来格式化和分析日期的具体类。它允许进行格式化(日期 -> 文本)、分析(文本 -> 日期)和规范化。
SimpleDateFormat 使得可以选择任何用户定义的日期-时间格式的模式。但是,仍然建议通过 DateFormat 中的 getTimeInstance、getDateInstance 或 getDateTimeInstance 来新的创建日期-时间格式化程序。
4、java.util.Calendar(抽象类)
Calendar 类是一个抽象类,它为特定瞬间与一组诸如 YEAR、MONTH、DAY_OF_MONTH、HOUR 等 日历字段之间的转换提供了一些方法,并为操作日历字段(例如获得下星期的日期)提供了一些方法。瞬间可用毫秒值来表示,它是距历元(即格林威治标准时间 1970 年 1 月 1 日的 00:00:00.000,格里高利历)的偏移量。
与其他语言环境敏感类一样,Calendar 提供了一个类方法 getInstance,以获得此类型的一个通用的对象。Calendar 的 getInstance 方法返回一个 Calendar 对象,其日历字段已由当前日期和时间初始化。
11.5.1. 计算从今天算起,150天之后是几月几号,并格式化成xxxx年xx月x日的形式打印出来。
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
public class DateUtil {
private static void get() {
Calendar calendar = Calendar.getInstance();
//当前日期加150天
calendar.add(Calendar.DATE, 150);
//calendar.getTime获取Date
Date date = calendar.getTime();
//SimpleDateFormat是DateFormat的直接子类
DateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
String dateStr = sdf.format(date);
System.out.println(dateStr);//2019年01月27日 13:16:40
try {
//字符串转日期
Date dateNow = sdf.parse("2018年8月31日 13:21:12");
Calendar calendar1 = Calendar.getInstance();
//calendar.setTime设置Date
calendar1.setTime(dateNow);
//after、before日期比较
System.out.println(calendar.after(calendar1));//true
System.out.println(calendar.before(calendar1));//false
//getTimeInMillis获取毫秒数
long f1 = calendar.getTimeInMillis(),f2 = calendar1.getTimeInMillis();
System.out.println((f1-f2)/1000/60/60/24);
} catch (ParseException e) {
e.printStackTrace();
}
Calendar calendar2 = Calendar.getInstance();
calendar2.set(Calendar.YEAR, 2012);
calendar2.set(Calendar.MONTH,0);//输入的数字超出范围,并不会报错,而是进行计算。如果没有设置DAY_OF_MONTH的话,则日期更离谱
calendar2.set(Calendar.DAY_OF_MONTH,2);
System.out.println(sdf.format(calendar2.getTime()));//2012年01月02日 13:37:54
//获取一个月有多少天
System.out.println(calendar2.getActualMaximum(Calendar.DATE));//31
}
public static void main(String[] args) {
get();
}
}
11.5.1.1. 判断闰年的方法
java.util
类 GregorianCalendar
java.lang.Object
java.util.Calendar
java.util.GregorianCalendar
isLeapYear
public boolean isLeapYear(int year)
确定给定的年份是否为闰年。如果给定的年份是闰年,则返回 true。要指定 BC 年份,必须给定 1 - 年份。例如,指定 -3 为 BC 4 年。
参数:
year - 给定的年份。
返回:
如果给定的年份为闰年,则返回 true;否则返回 false。
12. I/O流
◇◇◇字节流——InputStream,OutputStream◇◇◇
“流”就像一根管道
字节流表示管道中只能通过“字节数据”
InputStream 字节输入流:从InputStream 派生出去可以创建“字节数据输入流对象”的所有类,如FileInputStream,BufferedInputStream(发动机),ByteArray+InputStream,Data+InputStream,Object+InputStream都继承了“读取字节read()”方法,都能读取磁盘文件
OutputStream字节输出流:从OutputStream派生出去可以创建“字节数据输出流对象”的所有类,如FileOutputStream,BufferedOutputStream(发动机),ByteArray+OutputStream,Data+OutputStream,Object+OutputStream都继承了“写入字节write()”方法,都能写入磁盘文件
◇◇◇字符流——Reader,Writer◇◇◇
“流”就像一根管道
字符流表示管道中只能通过“字符数据”
Reader 字符输入流:从Reader派生出去可以创建“字符数据输入流对象”的所有类,如FileReader,BufferedReader(发动机),CharArrayReader,String+Reader,InputStreamReader。都继承了“读取字符read()”方法,都能读取磁盘文件
Writer 字符输出流:从Writer派生出去可以创建“字符数据输出流对象”的所有类,如FileWriter,BufferedWriter(发动机),CharArrayWriter,StringWriter,OutputStreamWriter。都继承了“写入字符write()”方法,都能写入磁盘文件◎FileOutputStream 字节输出流写文件◎
字节数据:byte 定义的变量就是字节数据
FileOutputStream就是写“字节数据”到文件中的类(只能写入“字节数据”,如果不是“字节数据”就需要转换getBytes()方法转换成字节数据)
import java.io.FileOutputStream;
import java.io.IOException;
public class Maintest {
public static void main(String[] args) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream("G:\\a.txt");
byte word = 65; // byte定义的word变量是字节数据,可以直接写入
fos.write(word);
String str = "早上好啊"; // String定义的是字符数据,要用getBytes()转换成字节数据,再用byte定义变量接收
byte[] words = str.getBytes();
fos.write(words);
System.out.println("写入成功!");
fos.close();
} catch (IOException e) {
}
}
}
◎FileWriter 字符输出流写文件◎
FileWriter有write()方法,写入字符数据到输出流文件中但效率不高,一般只用于打开路径,而“用BufferedWriter来写入”,BufferedWriter bw=new BufferedWriter(fw)“缓冲高效写入字符输出流文件”的类
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class Maintest {
public static void main(String[] args) {
FileWriter fw = null;
try {
fw = new FileWriter("G:\\a.txt");
BufferedWriter bw = new BufferedWriter(fw);
String str = "写入的是字符,可以直接写入"; // FileWriter写入的是字符数据,就是不用getbytes()转换成字节数据了
bw.write(str);
System.out.println("写入成功");
bw.close();
} catch (IOException e) {
}
}
}
12.1. RandomAccessFile
我的理解是:RandomAccessFile = File + OutputStream + InputStream + 指针位置操作
现有如下的一个需求,向已存在1G数据的txt文本里末尾追加一行文字,内容如下“Lucene是一款非常优秀的全文检索库”。可能大多数朋友会觉得这个需求很easy,说实话,确实easy,然后XXX君开始实现了,直接使用Java中的流读取了txt文本里原来所有的数据转成字符串后,然后拼接了“Lucene是一款非常优秀的全文检索库”,又写回文本里了,至此,大功告成。后来需求改了,向5G数据的txt文本里追加了,结果XXX君傻了,他内存只有4G,如果强制读取所有的数据并追加,会报内存溢出的异常。
其实上面的需求很简单,如果我们使用JAVA IO体系中的RandomAccessFile类来完成的话,可以实现零内存追加。
与普通的IO流相比,它最大的特别之处就是支持任意访问的方式,程序可以直接跳到任意地方来读写数据。
下面来看下RandomAccessFile类中比较重要的2个方法,其他的和普通IO类似
- getFilePointer():返回文件记录指针的当前位置
- seek(long pos):将文件记录指针定位到pos的位置
我们可以用RandomAccessFile这个类,来实现一个多线程断点下载的功能
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
public class RandomAccessFileTest {
/**
* 实现向指定位置 插入数据
*
* @param fileName
* 文件名
* @param points
* 指针位置
* @param insertContent
* 插入内容
**/
public static void insert(String fileName, long points, String insertContent) {
try {
File tmp = File.createTempFile("tmp", null);
tmp.deleteOnExit();// 在JVM退出时删除
RandomAccessFile raf = new RandomAccessFile(fileName, "rw");
// 创建一个临时文件夹来保存插入点后的数据
FileOutputStream tmpOut = new FileOutputStream(tmp);
FileInputStream tmpIn = new FileInputStream(tmp);
raf.seek(points);
/** 将插入点后的内容读入临时文件夹 **/
byte[] buff = new byte[1024];
// 用于保存临时读取的字节数
int hasRead = 0;
// 循环读取插入点后的内容
while ((hasRead = raf.read(buff)) > 0) {
// 将读取的数据写入临时文件中
tmpOut.write(buff, 0, hasRead);
}
// 插入需要指定添加的数据
raf.seek(points);// 返回原来的插入处
// 追加需要追加的内容
raf.write(insertContent.getBytes());
// 最后追加临时文件中的内容
while ((hasRead = tmpIn.read(buff)) > 0) {
raf.write(buff, 0, hasRead);
}
raf.close();
tmpOut.close();
tmpIn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void randomWrite(String path) {
try {
/** 以读写的方式建立一个RandomAccessFile对象 **/
RandomAccessFile raf = new RandomAccessFile(path, "rw");
// 将记录指针移动到文件最后
raf.seek(raf.length());
raf.write("我是追加的 \r\n".getBytes());
raf.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void t1() {
try {
/**
* model各个参数详解 r 代表以只读方式打开指定文件 rw 以读写方式打开指定文件 rws
* 读写方式打开,并对内容或元数据都同步写入底层存储设备 rwd 读写方式打开,对文件内容的更新同步更新至底层存储设备
**/
RandomAccessFile file = new RandomAccessFile("C:\\Users\\Administrator\\Desktop\\js.js", "r");
// 移动指针
file.seek(1000);
byte[] bs = new byte[1024];
// 用于保存实际读取的字节数
int hasRead = 0;
// 循环读取
while ((hasRead = file.read(bs)) > 0) {
// 打印读取的内容,并将字节转为字符串输入
System.out.println(new String(bs, 0, hasRead));
}
file.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
t1();
}
}
12.2. ByteArrayInputStream和ByteArrayOuitputStream
这两个流的作用在于:用IO流的方式来完成对字节数组的读写。
什么是内存虚拟文件或者内存映像文件?
将一块内存虚拟成一个硬盘上的文件,原来该写到硬盘文件上的内容会被写到这个内存中,原来改从一个硬盘文件上读取内容可以改为从内存中直接读取。(如果在程序运行过程中药产生过一些临时文件,就可以使用虚拟文件的方式来实现,不需要访问硬盘,而是直接访问内存)
12.3. ObjectInputStream和ObjectOutputStream
ObjectInputStream能够让你从输入流中读取Java对象,而不需要每次读取一个字节。你可以把InputStream包装到ObjectInputStream中,然后就可以从中读取对象了
ObjectOutputStream能够让你把对象写入到输出流中,而不需要每次写入一个字节。你可以把OutputStream包装到ObjectOutputStream中,然后就可以把对象写入到该输出流中了
12.4. DataInputStream和DataOutputStream
DataInputStream 是数据输入流。它继承于FilterInputStream。
DataInputStream 是用来装饰其它输入流,它 “允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型”。应用程序可以使用DataOutputStream(数据输出流)写入由DataInputStream(数据输入流)读取的数据。
12.5. BufferedInputStream和BufferedOutputStream
这两个类分别是FilterInputStream和FilterOutputStream的子类,作为装饰器子类,使用它们可以防止每次读取/发送数据时进行实际的写操作,代表着使用缓冲区。
我们有必要知道 **不带缓冲的操作,每读一个字节就要写入一个字节,由于涉及磁盘的IO操作相比内存的操作要慢很多,所以不带缓冲的流效率很低。**带缓冲的流,可以一次读很多字节,但不向磁盘中写入,只是先放到内存里。等凑够了缓冲区大小的时候一次性写入磁盘,这种方式可以减少磁盘操作次数,速度就会提高很多!
同时正因为它们实现了缓冲功能,所以要注意在使用BufferedOutputStream写完数据后,要调用flush()方法或close()方法,强行将缓冲区中的数据写出。否则可能无法写出数据。与之相似还BufferedReader和BufferedWriter两个类。
现在就可以回答在本文的开头提出的问题:
BufferedInputStream和BufferedOutputStream类就是实现了缓冲功能的输入流/输出流。使用带缓冲的输入输出流,效率更高,速度更快。
close()方法的作用
1、关闭输入流,并且释放系统资源
2、BufferedInputStream装饰一个 InputStream 使之具有缓冲功能,is要关闭只需要调用最终被装饰出的对象的 close()方法即可,因为它最终会调用真正数据源对象的 close()方法。因此,可以只调用外层流的close方法关闭其装饰的内层流。
那么如果我们想逐个关闭流,我们该怎么做?
答案是:先关闭外层流,再关闭内层流。一般情况下是:先打开的后关闭,后打开的先关闭;另一种情况:看依赖关系,如果流a依赖流b,应该先关闭流a,再关闭流b。例如处理流a依赖节点流b,应该先关闭处理流a,再关闭节点流b
12.6. BufferedReader属于哪种流,它主要是用来做什么的,它里面有那些经典的方法
解题思路:望文知意,Reader是字符流,而buffer是缓冲的作用,缓冲区是基于内存的,起到读写高效的作用;所以BufferedReader是高效字符流
BufferedReader是字符流,也是一种包装流,用来增强reader流.主要用来读取数据的,最经典的方法是readline,可以一次读一行,是reader不具备的.
12.7. 如果我要对字节流进行大量的从硬盘读取,要用那个流,为什么?
解题思路:因为明确说了是对字节流的读取,所以肯定是inputstream或者他的子类,又因为要大量读取,肯定要考虑到高效的问题,自然想到缓冲流。
用BufferedInputStream,原因:BufferedInputStream是InputStream的缓冲流,使用它可以防止每次读取数据时进行实际的写操作,代表着使用缓冲区。不带缓冲的操作,每读一个字节就要写入一个字节,由于涉及磁盘的IO操作相比内存的操作要慢很多,所以不带缓冲的流效率很低。带缓冲的流,可以一次读很多字节,但不向磁盘中写入,只是先放到内存里。等凑够了缓冲区大小的时候一次性写入磁盘,这种方式可以减少磁盘操作次数,速度就会提高很多!并且也可以减少对磁盘的损伤。
12.7.1. 什么是java序列化,如何实现java序列化?
序列化指的是 把一个Java对象,通过某种介质进行传输,比如Socket输入输出流,或者保存在一个文件里
实现java序列化的手段是让 该类实现接口 Serializable,这个接口是一个标识性接口,没有任何方法,仅仅用于表示该类可以序列化。
注意的是 被关键字static、transient修饰的变量不能被序列化。在被序列化后,transient修饰的变量会被设为初始值。如int型的是0、对象型的是null.
12.7.2. io流中用到的适配器模式和装饰者模式
解题思路:首先,要知道装饰者模式和适配器模式的作用;其次,可以自己举个例子把它的作用生动形象地讲出来;最后,简要说一下要完成这样的功能需要什么样的条件。
装饰器模式:就是动态地给一个对象添加一些额外的职责(对于原有功能的扩展)。
- 它必须持有一个被装饰的对象(作为成员变量)。
- 它必须拥有与被装饰对象相同的接口(多态调用、扩展需要)。
- 它可以给被装饰对象添加额外的功能。
比如,在io流中,FilterInputStream类就是装饰角色,它实现了InputStream类的所有接口,并持有InputStream的对象实例的引用,BufferedInputStream是具体的装饰器实现者,这个装饰器类的作用就是使得InputStream读取的数据保存在内存中,而提高读取的性能。
适配器模式:将一个类的接口转换成客户期望的另一个接口,让原本不兼容的接口可以合作无间。
1.适配器对象实现原有接口
2.适配器对象组合一个实现新接口的对象
3.对适配器原有接口方法的调用被委托给新接口的实例的特定方法(重写旧接口方法来调用新接口功能。)
比如,在io流中, InputStreamReader类继承了Reader接口,但要创建它必须在构造函数中传入一个InputStream的实例,InputStreamReader的作用也就是将InputStream适配到Reader。 InputStreamReader实现了Reader接口,并且持有了InputStream的引用。这里,适配器就是InputStreamReader类,而源角色就是InputStream代表的实例对象,目标接口就是Reader类。
适配器模式主要在于 将一个接口转变成另一个接口,它的目的是通过改变接口来达到重复使用的目的;
而装饰器模式不是要改变被装饰对象的接口,而是 保持原有的接口,但是增强原有对象的功能,或改变原有对象的方法而提高性能。
13. NIO
NIO是Java 4里面提供的新的API,目的是用来解决传统IO的问题。实际上,旧的IO包也已经使用nio重新实现过,以便充分利用这种速度提高。
旧IO中有三个类被修改了,分别是FileInputStream、FileOutputStream和RandomAccessFile,用于产生FileChannel。而Reader和Writer这种字符模式类不能用于产生通道,但是java.nio.channels.Channels类提供了实用方法,用以在通道中产生Reader和Writer。
NIO和IO最大的区别就是 IO是面向流的,NIO面向缓冲区的。IO的各种流是阻塞的,这意味着,当一个线程调用read和writer方法时,该线程将被阻塞,直到有一些数据被读取,或者数据完全被写入,该线程在此期间不能再干任何事。NIO是非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用数据,如果目前没有数据可用,就什么都不会获取,而不是保持线程阻塞,所以在变得数据可以获取之前,线程可以先继续做其他事情,非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写完,这个线程同时也可以去做其他事情,线程通常将非阻塞IO的空闲时间用于其他通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。IO是单线程的,而NIO是用选择器来模拟多线程的。
在FileInputStream中,有如下涉及FileChannel的源码:
......
private FileChannel channel;
......
public void close() throws IOException {
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}
if (channel != null) {
fd.decrementAndGetUseCount();
channel.close();
}
int useCount = fd.decrementAndGetUseCount();
if ((useCount <= 0) || !isRunningFinalize()) {
close0();
}
}
......
public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, false, true, append, this);
/*
* Increment fd's use count. Invoking the channel's close()
* method will result in decrementing the use count set for
* the channel.
*/
fd.incrementAndGetUseCount();
}
return channel;
}
}
13.1. NIO中的几个基础概念
在NIO中有几个比较关键的概念:Channel(通道),Buffer(缓冲区),Selector(选择器)。
- Channel:可以将NIO 中的Channel同传统IO中的Stream来类比,但是要注意,传统IO中,Stream是单向的,比如InputStream只能进行读取操作,OutputStream只能进行写操作。而Channel是双向的,既可用来进行读操作,又可用来进行写操作。
- Buffer(缓冲区),是NIO中非常重要的一个东西,在NIO中所有数据的读和写都离不开Buffer。比如上面的一段代码中,读取的数据时放在byte数组当中,而在NIO中,读取的数据只能放在Buffer中。同样地,写入数据也是先写入到Buffer中。
- NIO中最核心的一个东西:Selector。可以说它是NIO中最关键的一个部分,Selector的作用就是用来轮询每个注册的Channel,一旦发现Channel有注册的事件发生,便获取事件然后进行处理。
13.2. Channel
以下是常用的几种通道:
- FileChannel:可以从文件读或者向文件写入数据
- SocketChanel:以TCP来向网络连接的两端读写数据
- ServerSocketChannel:能够监听客户端发起的TCP连接,并为每个TCP连接创建一个新的SocketChannel来进行数据读写
- DatagramChannel:以UDP协议来向网络连接的两端读写数据。
下面给出通过FileChannel来向文件中写入数据的一个例子:
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOTest {
public static void main(String[] args) throws IOException {
File file = new File("C:\\Users\\Administrator\\Desktop\\test.txt");
//获取通道
FileOutputStream os = new FileOutputStream(file);
FileChannel channel = os.getChannel();
// 初始化Buffer,设定Buffer每次可以存储数据量
// 创建的Buffer是1024byte的,如果实际数据本身就小于1024,那么limit就是实际数据大小
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello kitty".getBytes());
// buffer.flip();一定得有,如果没有,就是从文件最后开始读取的,当然读出来的都是byte=0时候的字符。通过buffer.flip();这个语句,就能把buffer的当前位置更改为buffer缓冲区的第一个位置。
buffer.flip();
channel.write(buffer);
//关闭流和通道
channel.close();
os.close();
}
}
在调用channel的write方法之前必须调用buffer的flip方法,如果没有,就是从文件最后开始读取的,当然读出来的都是byte=0时候的字符。通过buffer.flip();这个语句,就能把buffer的当前位置更改为buffer缓冲区的第一个位置。
13.3. Buffer
Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。客户端发送数据时,必须先将数据存入Buffer中,然后将Buffer中的内容写入通道。服务端这边接收数据必须通过Channel将数据读入到Buffer中,然后再从Buffer中取出数据来处理。
Buffer类有几个重要的属性:
- capacity:作为一个内存快,其就代表了当前Buffer能最多暂存多少数据量,存储的数据类型则是根据上面的Buffer对象类型,一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据。
- position:代表当前数据读或者写出于哪个位置,读模式:被重置从0开始,最大值可能为capacity-1或者limit-1,写模式:被重置从0开始,最大值limit-1
- limit:读模式下:最多能往Buffer里读多少数据,写模式下:最多能往Buffer里写多少数据,limit大小跟数量大小和capacity有关
在NIO中,Buffer是一个顶层父类,它是一个抽象类,常用的Buffer的子类有:
- ByteBuffer
- IntBuffer
- CharBuffer
- LongBuffer
- DoubleBuffer
- FloatBuffer
- ShortBuffer
如果是对于文件读写,上面几种Buffer都可能会用到。但是对于网络读写来说,用的最多的是ByteBuffer。
Buffer读写数据步骤:
- 写入数据到Buffer(fileChannel.read(buf))
- 调用flip()方法,把buffer的当前位置更改为buffer缓冲区的第一个位置。
- 从Buffer中读取数据
- 调用clear()方法或者compact方法
Buffer的方法:
- flip():将Buffer写模式切换到读模式,并且将position置为0.
- clear():清除整个缓冲区
- compact():只会清除已经读过的数据,任何未读的数据都被转移到缓冲区起始处,新写入的数据将放到缓冲区未读数据的后面。
- allocate(1024):初始化Buffer,设定的值就是capacity的大小。
- rewind():position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素。
- mark()和reset():通过调用Buffer.mark()方法,可以标记Buffer中的一个特定的position。之后可以通过调用Buffer,reset()方法恢复到这个position
- equals():两个相等的Buffer,满足相同类型,剩余的元素数量相等,所剩余的元素也都相同。
13.4. Selector
Selector类是NIO的核心类,Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
与Selector有关的一个关键类是SelectionKey,一个SelectionKey表示一个到达的事件,这2个类构成了服务端处理业务的关键逻辑。
14. 动态代理
在Mybatis和spring的源码中都可以看到动态代理的使用(InvocationHandler接口的使用)
我觉得动态代理的精髓是:
不需要改动源码,也不需要继承,也不需要什么装饰模式、适配器模式,我就可以在目标方法执行之前、之后添加我要添加的动作。就像vue那样,在getter和setter时,执行额外的动作。
代理对象不需要实现接口,但是目标对象一定要实现接口,否则不能用动态代理
Spring主要有两大思想,一个是IoC(依赖注入),另一个就是AOP。AOP的原理就是java的动态代理机制
在java的动态代理机制中,有两个重要的类或接口,一个是 InvocationHandler(Interface)、另一个则是 Proxy(Class),这一个类和接口是实现我们动态代理所必须用到的。
每一个动态代理类都必须要实现InvocationHandler这个接口,并且每个代理类的实例都关联到了一个handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用。
Object invoke(Object proxy, Method method, Object[] args) throws Throwable方法一共接受三个参数
- proxy: 指代我们所代理的那个真实对象
- method: 指代的是我们所要调用真实对象的某个方法的Method对象
- args: 指代的是调用真实对象某个方法时接受的参数
Proxy这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但是我们用的最多的就是 newProxyInstance 这个方法:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
这个方法的作用就是得到一个动态的代理对象,其接收三个参数:
- loader: 一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象进行加载
- interfaces: 一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
- h: 一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上
其中interfaces中包含的接口,就是Proxy.newProxyInstance方法返回值可以被强制类型转换的接口类型。
实现一个动态代理的步骤
- 定义基础接口
- 定义类实现基础接口
- 定义动态代理,实现InvocationHandler的invoke方法
- 客户端调用,使用Proxy的newProxyInstance(classloader,interfaces,handler)获取代理方法并执行
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interface ISubject{
void read(String content);
void write(String content);
}
class Subject implements ISubject{
@Override
public void read(String content) {
System.out.println("读取内容:"+content);
}
@Override
public void write(String content) {
System.out.println("写入内容:"+content);
}
}
class DynamicProxyHandler implements InvocationHandler{
private Object obj;
public DynamicProxyHandler(Object obj) {
this.obj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("在代理方法执行之前,做一些事情");
method.invoke(obj, args);
System.out.println("在代理方法执行之后,做一些事情");
return null;
}
}
class Client{
public static void execute(){
ISubject subj = new Subject();
DynamicProxyHandler handler = new DynamicProxyHandler(subj);
//为什么我们这里可以将其转化为Subject类型的对象?原因就是在newProxyInstance这个方法的第二个参数上,
//我们给这个代理对象提供了一组什么接口,那么我这个代理对象就会实现了这组接口,这个时候我们当然可以将这个代理对象强制类型转化为这组接口中的任意一个,因为这里的接口是Subject类型,所以就可以将其转化为Subject类型了。
//
//同时我们一定要记住,通过 Proxy.newProxyInstance 创建的代理对象是在jvm运行时动态生成的一个对象,它并不是我们的InvocationHandler类型,也不是我们定义的那组接口的类型,而是在运行时动态生成的一个对象,并且命名方式都是这样的形式,以$开头,proxy为中,最后一个数字表示对象的标号。
ISubject subject = (ISubject)Proxy.newProxyInstance(handler.getClass().getClassLoader(), subj.getClass().getInterfaces(), handler);
//虽然这里只是显性的调用方法,和直接new对象并执行方法的外观一样。但是执行后就发现,不但完成了read、write的动作,还添加了我们在invoke中添加的额外动作
//所以这里并不是执行ISubject的实现类方法,而是执行代理的方法,顺便执行了实现类方法而已。换句话说,完全可以不执行实现类方法
//这里是通过代理对象来调用实现的那种接口中的方法,这个时候程序就会跳转到由这个代理对象关联到的 handler 中的invoke方法去执行,而我们的这个 handler 对象又接受了一个 RealSubject类型的参数,表示我要代理的就是这个真实对象,所以此时就会调用 handler 中的invoke方法去执行
subject.read("<<格林童话>>");
subject.write("<<天天日记>>");
}
}
public class ProxyTest {
public static void main(String[] args) {
Client.execute();
}
}
com.wanmei.meishu.ms.$Proxy0
在代理方法执行之前,做一些事情
读取内容:<<格林童话>>
在代理方法执行之后,做一些事情
在代理方法执行之前,做一些事情
写入内容:<<天天日记>>
在代理方法执行之后,做一些事情
15. 异常
15.0.0.1. 常见问题
15.0.0.1.1. Error和Exception有什么区别?
1 Error和Exception都实现了Throwable接口
2 Error指的是JVM层面的错误,比如内存不足OutOfMemoryError
3 Exception 指的是代码逻辑的异常,比如下标越界OutOfIndexException
15.0.0.1.2. try {}里有一个return语句,那么紧跟在这个try后的finally {}里的code会不会被执行,什么时候被执行,在return前还是后?
try里的return 和 finally里的return 都会执行,但是当前方法只会采纳finally中return的值
15.0.0.1.3. 给我五个你最常见到的runtime exception。
NullPointerException 空指针异常
ClassCastException 类型转换异常
IndexOutOfBoundsException 数组下标越界异常
ArithmeticException 算术异常,比如除数为零
ConcurrentModificationException 同步修改异常,遍历一个集合的时候,删除集合的元素,就会抛出该异常
NegativeArraySizeException 为数组分配的空间是负数异常
16. 常用API
16.1. Class
Class.forName的作用?为什么要用?
Class.forName常见的场景是在数据库驱动初始化的时候调用。
Class.forName本身的意义是加载类到JVM中。 一旦一个类被加载到JVM中,它的静态属性就会被初始化,在初始化的过程中就会执行相关代码,从而达到"加载驱动的效果"
16.2. Math
- static int|long|double|float abs(int|long|double|float a) :返回 float 值的绝对值。
- static int|long|double|float max|min(int a, int b):返回两个 int 值中较大(小)的一个。
- static double ceil(double a) :返回最小的(最接近负无穷大)double 值,该值大于等于参数,并等于某个整数。
- static floor(double a) :返回最大的(最接近正无穷大)double 值,该值小于等于参数,并等于某个整数。
- pow(double a, double b) :返回第一个参数的第二个参数次幂的值。
- random() :返回带正号的 double 值,该值大于等于 0.0 且小于 1.0。
- round(double a) :返回最接近参数的 long。(意思是+0.5 取整数)
16.3. Random
- public int nextInt(int n):返回一个伪随机数,它是取自此随机数生成器序列的、在 0(包括)和指定值(不包括)之间均匀分布的 int 值。nextInt 的常规协定是,伪随机地生成并返回指定范围中的一个 int 值。所有可能的 n 个 int 值的生成概率(大致)相同
16.4. Runtime
Runtime代表这Java运行时环境,每个Java都有一个Runtime实例。该类会被自动创建,可以通过Runtime.getRuntime
获取Java运行时环境
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class RuntimeUtil {
static Runtime runtime = Runtime.getRuntime();
public static void main(String[] args) {
memory() ;
System.out.println("-------------");
processors();
}
private static void processors() {
//核心线程数
System.out.println(runtime.availableProcessors());
BufferedReader reader=null;
//执行命令
try {
Process process = runtime.exec("ls /home/lihh");
//获取执行命令的结果
reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
process.waitFor();// 等待子进程完成再往下执行。
String line = null;
while ((line = reader.readLine()) != null) {
System.out.println(line + "\r\n");
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
if(reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private static void memory() {
int chg = 1024*1024;
//jvm已经从操作系统拿到多少内存
System.out.println(runtime.totalMemory()/chg+"mb");
//已经从操作系统中拿到的内存中有多少空闲内存
System.out.println(runtime.freeMemory()/chg+"mb");
//jvm可以从操作系统拿到的最大内存,即配置的Xmx参数。如果没有配置,默认为64mb
System.out.println(runtime.maxMemory()/chg+"mb");
}
}
import java.util.ArrayList;
import java.util.Random;
public class Demo1 {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
while(list.size() < 10){
Random random = new Random();
int nextInt = random.nextInt(20) + 1;
if(!list.contains(nextInt)) {
list.add(nextInt);
}
}
System.out.println(list);
}
}
17. JDBC
JDBC的全称是Java DataBase Connection,也就是Java数据库连接,我们可以用它来操作关系型数据库。JDBC接口及相关类在java.sql包和javax.sql包里。
JDBC接口让Java程序和JDBC驱动实现了松耦合,使得切换不同的数据库变得更加简单。
17.1. 使用JDBC步骤
加载JDBC驱动程序 -->建立数据库连接Connection --> 创建执行sql的语句Statement --> 处理执行结果 ResultSet --> 释放资源
17.1.1. JDBC是如何实现Java程序和JDBC驱动的松耦合的?
JDBC API使用Java的反射机制来实现Java程序和JDBC驱动的松耦合。随便看一个简单的JDBC示例,你会发现所有操作都是通过JDBC接口完成的,而 驱动只有在通过Class.forName反射机制来加载的时候才会出现。
17.2. Driver接口
Driver接口由数据库厂商提供,作为java开发人员,只需要使用Driver接口就可以了。在编程中要连接数据库必须先安装特定厂商的数据库驱动程序,不同的数据库有不同的装载方法。
- 装载Mysql驱动:Class.forName(“com.mysql.jdbc.Driver”)
- 装载Oracle驱动:Class.forName(“oracle.jabc.driver.OracleDriver”)
- 装载sqlite驱动:Class.forName(“org.sqlite.JDBC”);
17.3. DriverManager
这个类管理数据库驱动程序的列表。确定内容是否符合从 java 应用程序使用的通信子协议正确的数据库驱动程序的连接请求。识别JDBC子协议的第一个取定程序将别用来建立数据库连接。
注:DataSource 接口是 JDBC 2.0 API 中的新增内容,它提供了连接到数据源的另一种方法。使用 DataSource 对象是连接到数据源的首选方法。
17.4. Connection 接口
Connection 与特定数据库的连接(会话),在上下文中执行sql语句并返回结果。此接口有接触数据库的所有方法,连接对象便是通信上下文,即,与数据库中所有的通信都是通过此唯一的连接对象。
riverManager.getConnection(url,user,password) 方法来建立url 中定义的数据库来连接
如连接Mysql 数据库:Connection connection = DriverManager.getConnection(“jdbc:mysql://host:port/database”,“user”,“password”);
其他数据库类似
常用的方法:
- createStatement(): 创建向数据库发送sql的statement对象。
- prepareStatement(sql):创建相数据库发送预编译sql的PrepredStatement对象。
- prepareCall(sql) : 创建执行存储过程的callableStatement对象。
- setAutoCommit(boolean autoCommit) : 设置事务是否自动提交。
- commit() :在连接上提交事务
- rollback():在此连接上回滚事务
17.5. Statement和PrepareStatement
17.5.1. Statement 接口
用于执行静态sql语句并返回它所生成结果的对象
三种Statement 类:
- Statement 由createStatement创建,用于发送简单的sql语句(不带参数)
- PreparedStatement继承自Statement 接口,由prepareStatement(sql)创建,用于发送含有一个或者多个参数的sql语句。PreparedStatement对象采用预编译,比Statement对象效率更高。并且可以防止sql注入。
- CallableStatement:继承自PreparedStatement接口,由方法prepareCall()创建,用于存储调用过程。
常用的Statement方法:
- execute(String sql) 运行语句,返回是否有结果集
- executeQuery(String sql) 运行select语句,返回ResulySet结果集
- executeUpdate(String sql) 运行insert/delete/update 操作,返回更新的 行数。
- addBatch(String sql) 把多条sql语句放到一个批处理中
- executeBatch() 想数据库发送一批sql语句执行
17.5.2. execute,executeQuery,executeUpdate的区别是什么?
Statement的execute(String query)方法用来执行任意的SQL查询,如果查询的结果是一个ResultSet,这个方法就返回true。如果结果不是ResultSet,比如insert或者update查询,它就会返回false。我们可以通过它的getResultSet方法来获取ResultSet,或者通过getUpdateCount()方法来获取更新的记录条数。
Statement的executeQuery(String query)接口用来执行select查询,并且返回ResultSet。即使查询不到记录返回的ResultSet也不会为null。我们通常使用executeQuery来执行查询语句,这样的话如果传进来的是insert或者update语句的话,它会抛出错误信息为 “executeQuery method can not be used for update”的java.util.SQLException。
Statement的executeUpdate(String query)方法用来执行insert或者update/delete(DML)语句,或者 什么也不返回DDL语句。返回值是int类型,如果是DML语句的话,它就是更新的条数,如果是DDL的话,就返回0。
只有当你不确定是什么语句的时候才应该使用execute()方法,否则应该使用executeQuery或者executeUpdate方法。
17.5.3. Statement中的getGeneratedKeys方法有什么用?
有的时候表会生成主键,这时候就可以用Statement的getGeneratedKeys()方法来获取这个自动生成的主键的值了。
17.5.4. 相对于Statement,PreparedStatement的优点是什么?
它和Statement相比优点在于:
- PreparedStatement有助于防止SQL注入,因为它会自动对特殊字符转义。
- PreparedStatement可以用来进行动态查询。
- PreparedStatement执行更快。尤其当你重用它或者使用它的拼量查询接口执行多条语句时。
- 使用PreparedStatement的setter方法更容易写出面向对象的代码,而Statement的话,我们得拼接字符串来生成查询语句。如果参数太多了,字符串拼接看起来会非常丑陋并且容易出错。
17.6. ResultSet
查询结果集,提供了检索不同类型字段的方法
常用的有:
- getString(int index) / getString(String columnName) 获得在数据库里是varchar ,char 类型的数据对象
- getFloat
- getDate
- getBoolean
- getObjejct
ResultSet还提供了对结果集进行滚动的方法:
- next() 移动到下一行
- previous() 移动到前一行
- absolute(int row) 移动到指定行
- beforeFirst()移动到ResultSet 的最前面
- afterLast()移动到ResultSet 的最后面
使用后依次关闭对象及连接:ResultSet -> Statement ->Connection
17.6.1. JDBC的ResultSet是什么?
在查询数据库后会返回一个ResultSet,它就像是查询结果集的一张数据表。
ResultSet对象维护了一个游标,指向当前的数据行。开始的时候这个游标指向的是第一行。如果调用了ResultSet的next()方法游标会下移一行,如果没有更多的数据了,next()方法会返回false。可以在for循环中用它来遍历数据集。
默认的ResultSet是不能更新的,游标也只能往下移。也就是说你只能从第一行到最后一行遍历一遍。不过也可以创建可以回滚或者可更新的ResultSet,像下面这样。
Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
当生成ResultSet的Statement对象要关闭或者重新执行或是获取下一个ResultSet的时候,ResultSet对象也会自动关闭。
可以通过ResultSet的getter方法,传入列名或者从1开始的序号来获取列数据。
17.6.2. JDBC里的CLOB和BLOB数据类型分别代表什么?
CLOB意思是Character Large OBjects,字符大对象,它是由单字节字符组成的字符串数据,有自己专门的代码页。这种数据类型适用于存储超长的文本信息,那些可能会超出标准的VARCHAR数据类型长度限制(上限是32KB)的文本。
BLOB是Binary Larget OBject,它是二进制大对象,由二进制数据组成,没有专门的代码页。它能用于存储超过VARBINARY限制(32KB)的二进制数据。这种数据类型适合存储图片,声音,图形,或者其它业务程序特定的数据。
17.6.3. 什么是JDBC的最佳实践?
下面列举了其中的一些:
- 数据库资源是非常昂贵的,用完了应该尽快关闭它。Connection, Statement, ResultSet等JDBC对象都有close方法,调用它就好了。
- 养成在代码中显式关闭掉ResultSet,Statement,Connection的习惯,如果你用的是连接池的话,连接用完后会放回池里,但是没有关闭的ResultSet和Statement就会造成资源泄漏了。
- 在finally块中关闭资源,保证即便出了异常也能正常关闭。
- 大量类似的查询应当使用批处理完成。
- 尽量使用PreparedStatement而不是Statement,以避免SQL注入,同时还能通过预编译和缓存机制提升执行的效率。
- 如果你要将大量数据读入到ResultSet中,应该合理的设置fetchSize以便提升性能。
- 你用的数据库可能没有支持所有的隔离级别,用之前先仔细确认下。
- 数据库隔离级别越高性能越差,确保你的数据库连接设置的隔离级别是最优的。
- 如果在WEB程序中创建数据库连接,最好通过JNDI使用JDBC的数据源,这样可以对连接进行重用。
- 如果你需要长时间对ResultSet进行操作的话,尽量使用离线的RowSet。
17.6.4. 事务(ACID特点,隔离级别,提交,回滚)
事务的基本概念
一组要么同时成功,要么同时失败的sql语句,是数据库操作的一个基本执行单元。
事务开始于:
- 连接到数据库上,并执行一条DML语句(Insert/delete/update)。
- 前一个事务结束后,有输入了另外一条DML语句。
事务结束于: - 执行commit或rollback 语句。
- 执行一条DCL语句,例如create table 语句,在次此情况下,会自动执行commit语句
- 执行一条DCL语句,例如grant语句,在此情况下,会自动自行commit语句
- 断开与数据库的连接
- 执行了一条DML语句,该语句失败了,在此情况下,会为这个无效的DML语句执行rollback。
事务的四大特点
- atomicity 原子性:表示一个事务内的所有操作是一个整体,要么全成功,哟啊么全失败
- consistency 一致性:表示一个事务内有一个操作失败时,所有的更改过的数据都必须回滚到修改前的状态
- isolation 隔离性:事务 查看数据时数据所处的状态,要么时另一个并发事务修改它之前的状态,要么是另一事物修改它之后的状态,事务不会查看中间状态的数据。
- durability 持久性:持久性事务完成之后,它对于系统的影响是永久性的
事务的隔离级别
- 读取未提交数据
- 读取已提交数据
- 可重复读
- 序列化
18. 设计模式
参考:设计模式
总体来说设计模式分为三大类:
- 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
- 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
- 行为型模式,共十一种:策略模式、模板方法模式、责任链模式、观察者模式、迭代子模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
- 请列举出在JDK中几个常用的设计模式?
- 单例模式用于Runtime(待验证), Calendar和其他的一些类中。
- 工厂模式被用于各种不可变的类如Boolean,像Boolean.valueOf方法。(待验证)
- 观察者模式被用于swing和很多的时间监听中(待验证)。
- 装饰器模式被用于多个java IO类。
18.1. MVC 模式
MVC 模式代表 Model-View-Controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。
- Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑,在数据变化时更新控制器。
- View(视图) - 视图代表模型包含的数据的可视化。
- Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开
18.2. 创建型模式
这类模式都是以创建对象为目的。包括:工厂方法模式,抽象工厂模式,单例模式,建造者模式,原型模式。
18.2.1. 单例模式
- 属于工厂模式的特例,只是它不需要输入参数并且始终返回同一对象的引用。
- 能够保证某一类型对象在系统中的唯一性,即某类在系统中只有一个实例。
- 如何选择:如果单件模式实例在系统中经常会被用到,饿汉式是一个不错的选择;反之如果单件模式在系统中会很少用到或者几乎不会用到,那么懒汉式是一个不错的选择。
18.2.1.1. 饿汉模式
- 在程序启动或单件模式类被加载的时候,单件模式实例就已经被创建。
- 饿汉模式是线程安全的
下边是jdk中关于Calendar的一个单例模式
......
//Calendar的构造函数都是protected的
public static Calendar getInstance()
{
Calendar cal = createCalendar(TimeZone.getDefaultRef(), Locale.getDefault());
cal.sharedZone = true;
return cal;
}
......
public Simple(){
private static Single s=new Single();
private Single(){}
public static Simple getSimple(){
return s;
}
}
18.2.1.2. 懒汉模式
- 当程序第一次访问单件模式实例时才进行创建。
- 如果多个线程同时进入判断,就会生成多个实例对象
- 需要保证同步,付出效率的代价。
class Single{
private static Single s = null;
public Single() {
if (s == null)
s = new Single();
return s;
}
}
18.2.1.3. 懒汉双重锁模式
懒汉模式在使用时,容易引起不同步问题,所以应该创建同步"锁"
class Single1 {
private static Single1 s = null;
public Single1() {}
//同步函数的demo
public static synchronized Single1 getInstance() {
if (s == null)
s = new Single1();
return s;
}
//同步代码快的demo加锁,安全高效
public static Single1 getInStanceBlock(){
if(s==null)
synchronized (Single1.class) {
if(s==null)
s = new Single1();
}
return s;
}
}
18.2.2. 工厂模式
在Spring中,BeanFactory就是使用的工厂模式
工厂模式的最大好处是增加了创建对象时的封装层次。
如果你使用工厂来创建对象,之后你可以使用更高级和更高性能的实现来替换原始的产品实现或类,这不需要在调用层做任何修改。
18.2.2.1. 普通工厂模式
就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。
缺点:
- 每添加一个新的类型都要修改工厂类
interface Shape{
void draw();
}
class Circle implements Shape{
@Override
public void draw() {
System.out.println("draw circle");
}
}
class Rectangle implements Shape{
@Override
public void draw() {
System.out.println("draw rectangle");
}
}
class ShapeFactory{
public static Shape getShape(String type) {
if(type.equals("Circle"))
return new Circle();
if(type.equals("Rectangle"))
return new Rectangle();
return null;
}
}
public class FactoryTest {
public static void main(String[] args) {
Shape circle = ShapeFactory.getShape("Circle");
circle.draw();
Shape rectangle = ShapeFactory.getShape("Rectangle");
rectangle.draw();
}
}
18.2.2.2. 多个工厂方法模式
18.2.2.3. 静态工厂方法模式
18.2.2.4. 抽象工厂模式
interface Shape {
void draw();
}
class Circle implements Shape {
@Override
public void draw() {
System.out.println("draw circle");
}
}
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("draw rectangle");
}
}
interface Color {
void fill();
}
class Red implements Color {
@Override
public void fill() {
System.out.println("red");
}
}
class Blue implements Color {
@Override
public void fill() {
System.out.println("blue");
}
}
abstract class AbstractFactory {
abstract Color getColor(String type);
abstract Shape getShape(String type);
}
class ColorFactory extends AbstractFactory {
public Color getColor(String type) {
if (type.equals("Red"))
return new Red();
if (type.equals("Blue"))
return new Blue();
return null;
}
@Override
Shape getShape(String type) {
return null;
}
}
class ShapeFactory extends AbstractFactory {
public Color getColor(String type) {
return null;
}
@Override
Shape getShape(String type) {
if (type.equals("Circle"))
return new Circle();
if (type.equals("Rectangle"))
return new Rectangle();
return null;
}
}
class FactoryProducer {
public static AbstractFactory getFactory(String choice) {
if (choice.equalsIgnoreCase("SHAPE")) {
return new ShapeFactory();
} else if (choice.equalsIgnoreCase("COLOR")) {
return new ColorFactory();
}
return null;
}
}
public class FactoryTest {
public static void main(String[] args) {
// 获取形状工厂
AbstractFactory shapeFactory = FactoryProducer.getFactory("SHAPE");
// 获取形状为 Circle 的对象
Shape shape1 = shapeFactory.getShape("Circle");
// 调用 Circle 的 draw 方法
shape1.draw();
// 获取形状为 Rectangle 的对象
Shape shape2 = shapeFactory.getShape("Rectangle");
// 调用 Rectangle 的 draw 方法
shape2.draw();
// 获取颜色工厂
AbstractFactory colorFactory = FactoryProducer.getFactory("COLOR");
// 获取颜色为 Red 的对象
Color color1 = colorFactory.getColor("RED");
// 调用 Red 的 fill 方法
color1.fill();
// 获取颜色为 Blue 的对象
Color color3 = colorFactory.getColor("BLUE");
// 调用 Blue 的 fill 方法
color3.fill();
}
}
18.2.3. 建造者模式
意图:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
主要解决:主要解决在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
何时使用:一些基本部件不会变,而其组合经常变化的时候。
优点: 1、建造者独立,易扩展。 2、便于控制细节风险。
缺点: 1、产品必须有共同点,范围有限制。 2、如内部变化复杂,会有很多的建造类。
使用场景: 1、需要生成的对象具有复杂的内部结构。 2、需要生成的对象内部属性本身相互依赖。
注意事项:与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。
建造者模式通常包括下面几个角色:
- Builder:给出一个抽象接口,以规范产品对象的各个组成成分的建造。这个接口规定要实现复杂对象的哪些部分的创建,并不涉及具体的对象部件的创建。
- ConcreteBuilder:实现Builder接口,针对不同的商业逻辑,具体化复杂对象的各部分的创建。 在建造过程完成后,提供产品的实例。
- Director:调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。
- Product:要创建的复杂对象。
应用实例:
- 去肯德基,汉堡、可乐、薯条、炸鸡翅等是不变的,而其组合是经常变化的,生成出所谓的"套餐"。
- JAVA 中的 StringBuilder。
工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性。其实建造者模式就是抽象工厂模式的一种变体。
与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。
建造者模式和工厂模式同样是创建一个产品,工厂模式就是一个方法,而建造者模式有多个方法,并且建造者模式是有顺序的执行方法。就是说建造者模式强调的是顺序,而工厂模式没有顺序一说
我的理解是:工厂模式的关注点是 怎么创建一个实体类,建造者模式则是 怎么创建一个符合要求的实体类
//创建一个建筑实体
class Building{
private String basic;//地基
private String wall;//墙体
private String roof;//房顶
public String getBasic() {
return basic;
}
public void setBasic(String basic) {
this.basic = basic;
}
public String getWall() {
return wall;
}
public void setWall(String wall) {
this.wall = wall;
}
public String getRoof() {
return roof;
}
public void setRoof(String roof) {
this.roof = roof;
}
}
//创建建造者接口,以规范产品对象的各个组成成分的建造。这个接口规定要实现复杂对象的哪些部分的创建,并不涉及具体的对象部件的创建
interface IBuilder{
void buildBasic();
void buildWall();
void buildRoof();
Building createBuilding();
}
//创建建造者实现类。,针对不同的商业逻辑,具体化复杂对象的各部分的创建。 在建造过程完成后,提供产品的实例
class Builder implements IBuilder{
Building building;
public Builder(){
building = new Building();
}
@Override
public void buildBasic() {
building.setBasic("地基");
}
@Override
public void buildWall() {
building.setWall("墙体");
}
@Override
public void buildRoof() {
building.setRoof("屋顶");
}
@Override
public Building createBuilding() {
return this.building;
}
}
//组合套餐的组合方式。调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建
class Director {
public Building createBuildingDirector(IBuilder builder){
builder.buildBasic();
builder.buildWall();
builder.buildRoof();
return builder.createBuilding();
}
}
public class BuilderTest {
public static void main(String[] args) {
Building b = (new Director()).createBuildingDirector(new Builder());
System.out.println(b.getBasic());
}
}
JDK中建造者模式的应用:
StringBuilder和StringBuffer的append()方法使用了建造者模式。
StringBuilder把构建者的角色交给了其的父类AbstractStringBuilder,而StringBuilder则充当着Director的角色
如下,StringBuilder中两个append方法
public StringBuilder append(String str) {
super.append(str);
return this;
}
// Appends the specified string builder to this sequence.
private StringBuilder append(StringBuilder sb) {
if (sb == null)
return append("null");
int len = sb.length();
int newcount = count + len;
if (newcount > value.length)
expandCapacity(newcount);
sb.getChars(0, len, value, count);
count = newcount;
return this;
}
这两个方法最终分别调用了父类的append(String)和getChars方法
......
public AbstractStringBuilder append(String str) {
if (str == null) str = "null";
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
......
public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
{
if (srcBegin < 0)
throw new StringIndexOutOfBoundsException(srcBegin);
if ((srcEnd < 0) || (srcEnd > count))
throw new StringIndexOutOfBoundsException(srcEnd);
if (srcBegin > srcEnd)
throw new StringIndexOutOfBoundsException("srcBegin > srcEnd");
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
......
18.2.4. 原型模式
意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型对象创建新的对象
主要解决:在运行期建立和删除原型
何时使用:
- 当一个系统应该独立于它的产品创建,构成和表示时。
- 当要实例化的类是在运行时刻指定时,例如,通过动态装载。
- 为了避免创建一个与产品类层次平行的工厂类层次时。
- 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
如何解决:利用已有的一个原型对象,快速地生成和原型对象一样的实例。
关键代码:
1、实现克隆操作,在 JAVA 继承 Cloneable,重写 clone()。
2、原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些"易变类"拥有稳定的接口。
应用实例: 1、细胞分裂。 2、JAVA 中的 Object clone() 方法。
优点:
1、性能提高。
2、逃避构造函数的约束。
缺点:
- 配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
- 必须实现 Cloneable 接口。
- 逃避构造函数的约束。
使用场景:
- 资源优化场景。
- 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。
- 性能和安全要求的场景。
- 通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
- 一个对象多个修改者的场景。
- 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。
- 在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。
原型模式已经与 Java 融为浑然一体,大家可以随手拿来使用。
注意事项:
- 与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的,因此不会调用构造函数。
- 深拷贝与浅拷贝。Object类的clone方法只会拷贝对象中的基本的数据类型,对于数组、容器对象、引用对象等都不会拷贝,这就是浅拷贝。如果要实现深拷贝,必须将原型模式中的数组、容器对象、引用对象等另行拷贝
18.3. 结构型模式
这种模式以增强已存在对象功能为目的。包括:适配器模式,装饰器模式,代理模式,外观模式,桥接模式,组合模式,享元模式。
装饰器模式和适配器模式的差别:
装饰器模式和适配器模式都是包装模式,用来扩展已存在类的功能。
- 装饰器模式:扩展已有类的功能而不用改动原始类
- 适配器模式:作为两个不兼容接口之间的桥梁
18.3.1. 装饰器模式
装饰模式增加强了单个对象的能力。
Java IO 到处都使用了装饰模式,典型例子就是Buffered 系列类如BufferedReader和BufferedWriter,它们增强了Reader和Writer对象,以实现提升性能的 Buffer 层次的读取和写入。
允许向一个现有的对象中添加新的功能,同时又不改变结构。它作为现有类的包装类。可以代替继承
- 它必须具有一个装饰的对象。
- 它必须拥有与被装饰对象相同的接口。
- 它可以给被装饰对象添加额外的功能。
interface Sourcable {
void operation();
}
class Source implements Sourcable {
@Override
public void operation() {
System.out.println("原始类");
}
}
//第一个装饰器类
class SourceDescorator1 implements Sourcable {
private Sourcable sourcable;
public SourceDescorator1(Sourcable sourcable) {
this.sourcable = sourcable;
super();
}
@Override
public void operation() {
System.out.println("第一个装饰器 - f");
sourcable.operation();
System.out.println("第一个装饰器 - af");
}
}
//第二个装饰器类
class SourceDescorator2 implements Sourcable {
private Sourcable sourcable;
public SourceDescorator2(Sourcable sourcable) {
this.sourcable = sourcable;
super();
}
@Override
public void operation() {
System.out.println("第二个装饰器 - f");
sourcable.operation();
System.out.println("第二个装饰器 - af");
}
}
public class TestDecorator {
public static void main(String[] args) {
Sourcable source = new Source();
// 装饰类对象
Sourcable obj = new SourceDescorator2(new SourceDescorator1(source));
obj.operation();
}
}
第二个装饰器 - f
第一个装饰器 - f
原始类
第一个装饰器 - af
第二个装饰器 - af
18.3.2. 适配器模式
适配器模式不适合在详细设计阶段使用它,它是一种补偿模式,专用来在系统后期扩展、修改时所用。
优点:
- 可以让任何两个没有关联的类一起运行。
- 提高了类的复用。
- 增加了类的透明度。
- 灵活性好。
缺点:
- 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
- 由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。
18.3.2.1. 类适配器
//已经存在的类
class Voltage220 {
public void hasOut() {
System.out.println("输出220V电压!");
}
}
//标准接口
interface TargetVoltageI{
void out();
}
//实现了标准接口的普通类
class TargetVoltage implements TargetVoltageI{
@Override
public void out() {
System.out.println("输出5V电压");
}
}
//适配器类.继承已存在的类并实现标准接口
class Adapter extends Voltage220 implements TargetVoltageI{
@Override
public void out() {
//关键代码
super.hasOut();
}
}
public class AdapterModel {
public static void main(String[] args) {
TargetVoltageI common = new TargetVoltage();
common.out();
TargetVoltageI adapter = new Adapter();
adapter.out();
}
}
18.3.2.2. 对象适配器
使用直接关联,或者称为委托的方式。让类直接持有已经存在的类的对象
//已经存在的类
class Voltage220 {
public void hasOut() {
System.out.println("输出220V电压!");
}
}
//标准接口
interface TargetVoltageI{
void out();
}
//实现了标准接口的普通类
class TargetVoltage implements TargetVoltageI{
private Voltage220 voltage220;
public TargetVoltage(Voltage220 voltage220) {
this.voltage220 = voltage220;
}
@Override
public void out() {
voltage220.hasOut();
}
}
18.3.3. 代理模式
参看10. 动态代理
代理模式是指客户端并不直接调用实际的对象,而是通过调用代理,来间接的调用实际的对象。
为什么要采用这种间接的形式来调用对象呢?一般是因为客户端不想直接访问实际的对象,或者访问实际的对象存在困难,因此通过一个代理对象来完成间接的访问。
在现实生活中,这种情形非常的常见,比如请一个律师代理来打官司。
参考:代理模式
代理模式包含如下角色:
- Subject:抽象主题角色。可以是接口,也可以是抽象类。
- RealSubject:真实主题角色。业务逻辑的具体执行者。
- ProxySubject:代理主题角色。内部含有RealSubject的引用,负责对真实角色的调用,并在真实主题角色处理前后做预处理和善后工作。
18.3.3.1. 静态代理
缺点
因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多.同时,一旦接口增加方法,目标对象与代理对象都要维护.
//抽象角色
interface ICoder{
void implDemand(String demandName);
}
//真实角色
class JavaCoder implements ICoder{
private String coderName;
public JavaCoder(String coderName) {
this.coderName = coderName;
}
@Override
public void implDemand(String demandName){
System.out.println(coderName+" implements demand :"+demandName);
}
}
//代理角色
class CoderProxy implements ICoder{
private ICoder coder;
public CoderProxy(ICoder coder) {
this.coder = coder;
}
@Override
public void implDemand(String demandName) {
//todo:这里可以添加代理自己的动作包裹被代理者的动作
coder.implDemand(demandName);
}
}
public class ProxyModel {
public static void main(String[] args) {
ICoder coder = new JavaCoder("Zhang");
ICoder proxy = new CoderProxy(coder);
proxy.implDemand("Add user manageMent");
}
}
18.3.3.2. 动态代理
动态代理也叫做:JDK代理,接口代理
AOP用的恰恰是动态代理。
代理类在程序运行时创建的代理方式被称为动态代理。也就是说,代理类并不需要在Java代码中定义,而是在运行时动态生成的。相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类的函数。
与静态代理相比,抽象角色、真实角色都没有变化。变化的只有代理类。
总结一下,一个典型的动态代理可分为以下四个步骤:
- 创建抽象角色
- 创建真实角色
- 通过实现InvocationHandler接口创建中介类
- 通过场景类,动态生成代理类
在使用动态代理时,我们需要定义一个位于代理类与委托类之间的中介类,也叫动态代理类,这个类被要求实现InvocationHandler接口:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
//1.抽象角色
interface ICoder{
void implDemand(String demandName);
}
//2.真实角色
class JavaCoder implements ICoder{
private String coderName;
public JavaCoder(String coderName) {
this.coderName = coderName;
}
@Override
public void implDemand(String demandName){
System.out.println(coderName+" implements demand :"+demandName);
}
}
//3.动态代理类被要求实现接口InvocationHandler
class CoderDynamicProxy implements InvocationHandler{
private ICoder coder;
public CoderDynamicProxy(ICoder coder) {
this.coder = coder;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(System.currentTimeMillis());
Object result = method.invoke(coder, args);
System.out.println(System.currentTimeMillis());
return result;
}
}
public class ProxyModel {
public static void main(String[] args) {
// 4.
//要代理的真实对象
ICoder coder = new JavaCoder("Zhang");
//创建中介类实例
InvocationHandler handler = new CoderDynamicProxy(coder);
//获取类加载器
ClassLoader cl = coder.getClass().getClassLoader();
//动态产生一个代理类
ICoder proxy = (ICoder) Proxy.newProxyInstance(cl, coder.getClass().getInterfaces(), handler);
//通过代理类,执行doSomething方法;
proxy.implDemand("Modify user management");
}
}
18.3.3.3. Cglib代理
JDK的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能实现JDK的动态代理,cglib是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。
Cglib代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展
Cglib子类代理实现方法:
- 需要引入cglib的jar文件,但是Spring的核心包中已经包括了Cglib功能,所以直接引入Spring-core.jar即可.
- 引入功能包后,就可以在内存中动态构建子类
- 代理的类不能为final,否则报错
- 目标对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法.
18.4. 行为型模式
这些设计模式特别关注对象之间的通信。包括:行为型模式(11种):策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
18.4.1. 责任链模式
责任链模式,有多个对象,每个对象持有对下一个对象的引用,这样就会形成一条链,请求在这条链上传递,直到某一对象决定处理该请求。但是发出者并不清楚到底最终那个对象会处理该请求,所以,责任链模式可以实现,在隐瞒客户端的情况下,对系统进行动态的调整。
责任链模式在Tomcat中的应用
举个很简单的例子,就是报销流程,项目经理<部门经理<总经理
其中项目经理报销额度不能大于500,则部门经理的报销额度是不大于1000,超过1000则需要总经理审核
abstract class ConsumeHandler {
private ConsumeHandler nextHandler;
public ConsumeHandler getNextHandler() {
return nextHandler;
}
public void setNextHandler(ConsumeHandler nextHandler) {
this.nextHandler = nextHandler;
}
/** user申请人 free报销费用 */
public abstract void doHandler(String user, double free);
public static void main(String[] args) {
ProjectHandler projectHandler = new ProjectHandler();
DeptHandler deptHandler = new DeptHandler();
GeneralHandler generalHandler = new GeneralHandler();
projectHandler.setNextHandler(deptHandler);
deptHandler.setNextHandler(generalHandler);
projectHandler.doHandler("lwx", 450);
projectHandler.doHandler("lwx", 600);
projectHandler.doHandler("zy", 600);
projectHandler.doHandler("zy", 1500);
projectHandler.doHandler("lwxzy", 1500);
}
}
// 项目经理
class ProjectHandler extends ConsumeHandler {
@Override
public void doHandler(String user, double free) {
if (free < 500) {
if (user.equals("lwx")) {
System.out.println("项目经理:给予报销:" + free);
} else {
System.out.println("项目经理:报销不通过");
}
} else {
if (getNextHandler() != null) {
getNextHandler().doHandler(user, free);
}
}
}
}
// 部门经理
class DeptHandler extends ConsumeHandler {
@Override
public void doHandler(String user, double free) {
if (free < 1000) {
if (user.equals("zy")) {
System.out.println("部门经理:给予报销:" + free);
} else {
System.out.println("部门经理:报销不通过");
}
} else {
if (getNextHandler() != null) {
getNextHandler().doHandler(user, free);
}
}
}
}
// 总经理
class GeneralHandler extends ConsumeHandler {
@Override
public void doHandler(String user, double free) {
if (free >= 1000) {
if (user.equals("lwxzy")) {
System.out.println("总经理:给予报销:" + free);
} else {
System.out.println("总经理:报销不通过");
}
} else {
if (getNextHandler() != null) {
getNextHandler().doHandler(user, free);
}
}
}
}
18.4.2. 观察者模式
首先,弄明白两组概念:观察者(Observer)与被观察者(subject)、发布者(publicsher)与订阅者(subscriber)。这是相似的两组概念,讲的时候,要对应于各自所在的组,不要弄混了。
在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并自动更新。(这和前端vue的思想相同)
其实就是发布订阅模式,发布者发布信息,订阅者获取信息,订阅了就能收到信息,没订阅就收不到信息。
package com.wanmei.meishu.ms;
import java.util.ArrayList;
import java.util.List;
//观察者接口
interface Observable{
//观察
void addSub(ISubject sub);
//取消观察
void removeSub(ISubject sub,String msg);
//读取消息
void watch(String msg);
//获取观察者名称
String getName();
}
//观察者实例
class Observer implements Observable {
private String name;
public Observer(String name) {
this.name = name;
}
@Override
public void addSub(ISubject sub){
sub.addObserver(this);
System.out.println("Observer:用户【"+this.name+"】 订阅了消息");
}
@Override
public void removeSub(ISubject sub,String msg){
sub.removeObserver(this);
System.out.println("Observer:用户【"+this.name+"】 取消了订阅消息," + (msg == null ? "" : ("并说:" + msg)));
}
@Override
public void watch(String msg) {
System.out.println("Observer:用户【"+this.name+"】读取到的订阅消息是:" + msg);
}
public String getName() {
return name;
}
}
//被观察者接口
interface ISubject{
//给观察者们发送消息
void sendMsg(String msg);
//添加一个观察者
void addObserver(Observable user);
//取消一个观察者
void removeObserver(Observable user);
}
//被观察者实现方式
class SubjectImpl implements ISubject{
//持有观察者队列
private List<Observable> observerList;
//添加一个观察者
public synchronized void addObserver(Observable user){
if(observerList == null){
observerList = new ArrayList<Observable>();
}
observerList.add(user);
String str = "";
for (Observable observable : observerList) {
str+= observable.getName()+"、";
}
System.out.println("ISubject:目前已有用户:" + str.substring(0, str.length()-1));
}
//取消一个观察者
public void removeObserver(Observable user){
observerList.remove(user);
if(!observerList.isEmpty()){
String str = "";
for (Observable observable : observerList) {
str+= observable.getName()+"、";
}
System.out.println("ISubject:目前剩余用户:" + str.substring(0, str.length()- 1));
}
}
@Override
public void sendMsg(String msg) {
if(!observerList.isEmpty()){
System.out.println("ISubject:发送消息:" + msg);
for (Observable observable : observerList) {
observable.watch(msg);
}
}
}
}
public class ObserverTest {
public static void main(String[] args) {
ISubject sub = new SubjectImpl();
//第一个观察者
Observable u1 = new Observer("吴文俊");
u1.addSub(sub);
Observable u2 = new Observer("吴华云");
u2.addSub(sub);
Observable u3 = new Observer("李爪哇");
u3.addSub(sub);
sub.sendMsg("PHP是世界上最好的语言!");
u3.removeSub(sub,"去死吧,PHP");
sub.sendMsg("PHP是世界上最好的语言!");
}
}
运行结果
ISubject:目前已有用户:吴文俊
Observer:用户【吴文俊】 订阅了消息
ISubject:目前已有用户:吴文俊、吴华云
Observer:用户【吴华云】 订阅了消息
ISubject:目前已有用户:吴文俊、吴华云、李爪哇
Observer:用户【李爪哇】 订阅了消息
ISubject:发送消息:PHP是世界上最好的语言!
Observer:用户【吴文俊】读取到的订阅消息是:PHP是世界上最好的语言!
Observer:用户【吴华云】读取到的订阅消息是:PHP是世界上最好的语言!
Observer:用户【李爪哇】读取到的订阅消息是:PHP是世界上最好的语言!
ISubject:目前剩余用户:吴文俊、吴华云
Observer:用户【李爪哇】 取消了订阅消息并说:去死吧,PHP
ISubject:发送消息:PHP是世界上最好的语言!
Observer:用户【吴文俊】读取到的订阅消息是:PHP是世界上最好的语言!
Observer:用户【吴华云】读取到的订阅消息是:PHP是世界上最好的语言!
19. 六大原则
19.1. 开闭原则(OCP)
开闭原则(Open Closed Principle)是Java世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。
20. 框架
Spring+Spring MVC +Mybatis
20.1. Spring
使用Spring框架的好处是什么?
- 轻量:Spring 是轻量的,基本的版本大约2MB。
- 控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们。
- 面向切面的编程(AOP):Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。
- 容器:Spring 包含并管理应用中对象的生命周期和配置。
- MVC框架:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。
- 事务管理:Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)。
- 异常处理:Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转化为一致的unchecked 异常。
20.1.1. Spring AOP
AOP使用场景:权限控制、异常处理、缓存、事务管理、日志记录、数据校验等等
AOP基本概念
- 切面(Aspect): 程序运行过程中的某个的步骤或者阶段
- 连接点(Joinpoint): 程序运行过程中可执行特定处理(增强处理)的点, 如异常处理。而在SpringAOP中,方法调用是连接点。
- Advice(通知、处理、增强处理): 在符合的连接点进行的特定处理 (增强处理)
- 切入点(Pointcut): 可切入进行增强处理的连接点。AOP核心之一就是如何用表达式来定义符合的切入点。在Spring中,默认使用AspectJ的切入点语法。
由于Spring AOP只支持以Spring Bean的方法调用来作为连接点, 所以在这里切入点的定义包括:
- 切入点表达式, 来限制该能作用的范围大小,即是,能匹配哪些bean的方法
- 命名切入点
20.1.1.1. AspectJ
参考:Spring AOP @Before @Around @After 等 advice 的执行顺序
执行顺序
- 无异常情况:Around->Before->自己的method->Around->After->AfterReturning
- 异常情况:Around->Before->自己的method->Around->After->AfterThrowing
多个Aspect作用于一个方法上,如何指定每个 aspect 的执行顺序呢?
方法有两种:
- 实现org.springframework.core.Ordered接口,实现它的getOrder()方法
- 给aspect添加@Order注解,该注解全称为:org.springframework.core.annotation.Order
@Order(5)
@Component
@Aspect
public class Aspect1 {}
@Order(6)
@Component
@Aspect
public class Aspect2 {}
注意点
- 如果在同一个 aspect 类中,针对同一个 pointcut,定义了两个相同的 advice(比如,定义了两个 @Before),那么这两个 advice 的执行顺序是无法确定的,哪怕你给这两个 advice 添加了 @Order 这个注解,也不行。这点切记。
- 对于@Around这个advice,不管它有没有返回值,但是必须要方法内部,调用一下 pjp.proceed();否则,Controller 中的接口将没有机会被执行,从而也导致了 @Before这个advice不会被触发。
使用步骤
20.1.1.1.1. 引入aspectj的相关jar包
<!--使用AspectJ方式注解需要相应的包-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
</dependency>
<!--使用AspectJ方式注解需要相应的包-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
20.1.1.1.2. 在spring中启用aspectj
<aop:aspectj-autoproxy proxy-target-class="true" />
20.1.1.1.3. 编写注解
import java.lang.annotation.*;
/**
* api拦截器,记录日志
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented//文档生成时,该注解将被包含在javadoc中,可去掉
public @interface ApiInf {
}
20.1.1.1.4. 自定义实现Aspect
注意注解@Aspect,如果没有使用@Component,则需要在spring中注册该aspect的bean
<bean class="com.ufgov.util.rest.ApiLogAspect" />
20.1.1.1.4.1. Pointcut
- @Pointcut:Pointcut的定义包括两部分:Pointcut表达式(expression:就是@Pointcut后边的参数)和Pointcut签名(就是@Pontcut所在的方法名称),Pointcut签名在下边@Before和@After等中使用(相当于使用Pointcut表达式)。
20.1.1.1.4.2. #execution表示式(方法描述匹配)
表达式类型
标准的Aspectj Aop的pointcut的表达式类型是很丰富的,但是Spring Aop只支持其中的9种,外加Spring Aop自己扩充的一种一共是10种类型的表达式,分别如下。
- execution:一般用于指定方法的执行,用的最多。
- within:指定某些类型的全部方法执行,也可用来指定一个包。
- this:Spring Aop是基于代理的,生成的bean也是一个代理对象,this就是这个代理对象,当这个对象可以转换为指定的类型时,对应的切入点就是它了,Spring Aop将生效。
- target:当被代理的对象可以转换为指定的类型时,对应的切入点就是它了,Spring Aop将生效。
- args:当执行的方法的参数是指定类型时生效。
- @target:当代理的目标对象上拥有指定的注解时生效。
- @args:当执行的方法参数类型上拥有指定的注解时生效。
- @within:与@target类似,看官方文档和网上的说法都是@within只需要目标对象的类或者父类上有指定的注解,则@within会生效,而@target则是必须是目标对象的类上有指定的注解。而根据笔者的测试这两者都是只要目标类或父类上有指定的注解即可。
- @annotation:当执行的方法上拥有指定的注解时生效。
- bean:当调用的方法是指定的bean的方法时生效。
其实execution表示式的定义方式就是方法定义的全量方式
格式
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
括号中各个pattern分别表示:
- 修饰符匹配(modifier-pattern?)
- 返回值匹配(ret-type-pattern)可以为*表示任何返回值,全路径的类名等
- 类路径匹配(declaring-type-pattern?)
- 方法名匹配(name-pattern)可以指定方法名 或者 代表所有, set 代表以set开头的所有方法
- 参数匹配((param-pattern))可以指定具体的参数类型,多个参数间用“,”隔开,各个参数也可以用“”来表示匹配任意类型的参数,如(String)表示匹配一个String参数的方法;(,String) 表示匹配有两个参数的方法,第一个参数可以是任意类型,而第二个参数是String类型;可以用(…)表示零个或多个任意参数
- 异常类型匹配(throws-pattern?)
其中后面跟着“?”的是可选项
例子
1)execution(* *(..))//表示匹配所有方法
2)execution(public * com.savage.service.UserService.*(..))//表示匹配com.savage.server.UserService中所有的公有方法
3)execution(* com.savage.server..*.*(..))//表示匹配com.savage.server包及其子包下的所有方法
**> 最靠近(…)的为方法名,靠近.(…))的为类名或者接口名,如上例的JoinPointObjP2.(…))
参考:Spring AOP 中pointcut expression表达式解析及配置
20.1.1.1.4.3. 方法参数匹配
args()
@args()
//参数带有@MyMethodAnnotation标注的方法.
@args(com.elong.annotation.MyMethodAnnotation)
//参数为String类型(运行时决定)的方法.
args(String)
20.1.1.1.4.4. 当前AOP代理对象类型匹配
this()
20.1.1.1.4.5. 目标类匹配
target()
@target()
within()
@within()
//pointcutexp包里的任意类.
within(com.test.spring.aop.pointcutexp.*)
//pointcutexp包和所有子包里的任意类.
within(com.test.spring.aop.pointcutexp..*)
//实现了MyInterface接口的所有类,如果MyInterface不是接口,限定MyInterface单个类.
this(com.test.spring.aop.pointcutexp.MyInterface)
**> 当一个实现了接口的类被AOP的时候,用getBean方法必须cast为接口类型,不能为该类的类型.
20.1.1.1.4.6. 标有此注解的方法匹配
@annotation()
//带有@MyTypeAnnotation标注的所有类的任意方法.
@within(com.elong.annotation.MyTypeAnnotation)
@target(com.elong.annotation.MyTypeAnnotation)
//带有@MyTypeAnnotation标注的任意方法.
@annotation(com.elong.annotation.MyTypeAnnotation)
**> @within和@target针对类的注解,@annotation是针对方法的注解
@Before("og()")
这种使用方式等同于以下方式,直接定义execution表达式使用
@Before("execution(* com.savage.aop.MessageSender.*(..))")
Pointcut定义时,还可以使用&&、||、! 这三个运算
@Pointcut("execution(* com.savage.aop.MessageSender.*(..))")
private void logSender(){}
@Pointcut("execution(* com.savage.aop.MessageReceiver.*(..))")
private void logReceiver(){}
@Pointcut("logSender() || logReceiver()")
private void logMessage(){}
还可以将一些公用的Pointcut放到一个类中,以供整个应用程序使用,如下:
package com.savage.aop;
import org.aspectj.lang.annotation.*;
public class Pointcuts {
@Pointcut("execution(* *Message(..))")
public void logMessage(){}
@Pointcut("execution(* *Attachment(..))")
public void logAttachment(){}
@Pointcut("execution(* *Service.*(..))")
public void auth(){}
}
在使用上面定义Pointcut时,指定完整的类名加上Pointcut签名就可以了,如:
package com.savage.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect
public class LogBeforeAdvice {
@Before("com.sagage.aop.Pointcuts.logMessage()")
public void before(JoinPoint joinPoint) {
System.out.println("Logging before " + joinPoint.getSignature().getName());
}
}
也可以使用xml配置
<aop:config>
<aop:pointcut id="log" expression="execution(* com.savage.simplespring.bean.MessageSender.*(..))"/>
<aop:aspect id="logging" ref="logBeforeAdvice">
<aop:before pointcut-ref="log" method="before"/>
<aop:after-returning pointcut-ref="log" method="afterReturning"/>
</aop:aspect>
</aop:config>
package com.ufgov.util.rest;
/**
* 记录api访问日志的切面
*/
@Aspect
public class ApiLogAspect{
private static Logger logger = Logger.getLogger(ApiLogAspect.class);
/**
* 定义切入点
*/
@Pointcut("@annotation(ApiInf)")//这里就是表达式
//Pointcut签名
public void controllerAspect() {}
@Around("controllerAspect()")
public Object around(ProceedingJoinPoint point) throws Throwable {
// TODO something
return point.proceed(); // 不调用point.proceed()不会执行目标方法
}
/**
* 进入方法之前处理
*/
@Before("controllerAspect()")
public void doBefore() throws UnsupportedEncodingException{
......
}
/**
* 记录返回信息
*/
@AfterReturning(pointcut="controllerAspect()",returning="ret")
public void doAfterReturn(Object ret) {
//对返回数据进行格式化处理,用于入库
}
/**
* 记录发生的异常信息
* @param e
*/
@AfterThrowing(value="controllerAspect()",throwing="e")
public void doAfterThrow(Throwable e) {
e.printStackTrace();
......
}
}
20.1.1.1.5. 在目标方法上添加注解
@ApiInfo
....
20.2. Spring mvc运行原理
运行原理:
- springmvc将所有的请求都提交给DispatcherServlet,它会委托应用系统的其他模块负责对请求进行真正的处理工作。
- DispatcherServlet查询一个或多个HandlerMapping,找到处理请求的Controller.
- DispatcherServlet将请求提交到目标Controller
- Controller进行业务逻辑处理后,会返回一个ModelAndView
- DispathcherServlet查询一个或多个ViewResolver视图解析器,找到ModelAndView对象指定的视图对象;
- 视图负责将结果显示到客户端;视图对象负责渲染返回给客户端。
DispatcherServlet是整个Spring MVC的核心。它负责接收HTTP请求组织协调Spring MVC的各个组成部分。其主要工作有以下三项:
- 截获符合特定格式的URL请求。
- 初始化DispatcherServlet上下文对应的WebApplicationContext,并将其与业务层、持久化层的WebApplicationContext建立关联。
- 初始化Spring MVC的各个组成组件,并装配到DispatcherServlet中。
DispatcherServlet:前端控制器;(相当于一个转发器,中央处理器,调度)
ModelAndView:模型和视图的结合体;(Spring mvc的底层对象)
HandlerMapping: 处理器映射器;
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:conf/spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
20.3. Mybatis
在Mybatis的使用中,我们只写了DAO接口和XML配置文件,怎么没写实现类?原理是什么?
mybatis通过JDK的动态代理方式,在启动加载配置文件时,根据配置mapper的xml去生成Dao的实现。
session.getMapper()使用了代理,当调用一次此方法,都会产生一个代理class的instance,看看这个代理class的实现.
注意在spring中的配置
<!-- mybatis文件配置,扫描所有mapper文件 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"
p:dataSource-ref="dataSource"
p:configLocation="classpath:conf/mybatis-config.xml"
p:mapperLocations="classpath:mapper/*.xml" /><!-- configLocation为mybatis属性
mapperLocations为所有mapper -->
<!-- spring与mybatis整合配置,扫描所有dao ,生成与DAO类相同名字的bean(除了首字母小写) -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"
p:basePackage="com.ufgov.mapper" p:sqlSessionFactoryBeanName="sqlSessionFactory" />
其中的两个class就是mybatis的入口,初始化mybatis的配置。然后在spring装配service时,需要实例化dao,就由MapperScannerConfigurer来接管。对于方法的执行则由org.apache.ibatis.binding包下的MapperProxy、MapperProxyFactory、MapperRegistry、MapperMethod来处理
如果我们需要在DAO层写一些代码的话,这种方式就无能为力了。此时,MyBatis-Spring提供给我们的SqlSessionDaoSupport类就派上了用场。
关于mybatis中的dao是怎么实例化的,可以参考:Mapper(DAO层)接口如何实例化