反射
- 反射是在类运行时动态获取类型的信息。比如接口信息、成员信息、方法信息、构造函数信息等。根据动态获取到的信息创建对象、访问/修改成员、调用方法等。
- 使用反射可以得到类的名称,调用方法getName()。其中还包括getSimpleName(),getCanonicalName()以及getPackage()。
- 类中定义的静态和实例变量称之为字段,使用Field类表示,可以获取字段的访问权限,设置访问权限,获得字段值以及设置字段值。除此之外,还可以获得字段的修饰符以及基本类型。需要注意的是使用反射返回的修饰符是一个整型。如果需要查看具体的返回值类型,可以使用Modifier进行再次判断。
- 通过反射也可以获取类中的方法信息:对于invoke方法而言,如果Method是静态方法,则obj被忽略为null,args可以为null;
- 创建对象和构造方法:
public T newInstance() throws InstantiationException, IllegalAccessException
;
- 类型检查和转换:使用
instanceof
关键字可以用于判断变量指向的实际对象类型,instanceof后面的类型是在代码中确定的;
- 除此之外,Class方法中还有其他的一些方法可以用于获取类的声明信息、修饰符、父类、接口以及注解等;
- 对于数组类型可以使用
public native Class<T> getComponentType()
获取其元素类型;
- 反射与枚举:枚举类型也有一个专门的方法:
public T[] getEnumConstants()
;
- 反射与泛型:可以获取泛型参数的信息:
- Class类有如下方法:
public TypeVariable<Class<T>>[] getTypeParameters()
- Field有如下方法:
public Type getGenericType()
- Method有如下方法:
public Type getGenericReturnType()
,public Type[] getGenericParameterTypes()
和public Type[] getGenericExceptionTypes()
- Constructor有如下方法:
public Type[] getGenericParameterTypes()
- Type实现了Class方法,其中还实现了下列方法:
- TypeVariable类型参数可以有上界;
- ParameterizedType参数化的类型,有原始类型和具体类型;
- WildcardType通配符类型
- 通过反射获取泛型示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class GenericDemo { static class GenericTest<U extends Comparable<U, V> { U u; V v; List<String> list; public U test(List<? extends Number> numbers) { return null; } } public static void main(String[] args) { Class<?> cls = GenericTest.class; for (TypeVariable t : cls.getTypeParameters()) { System.out.println(t.getName() + " extends " + Arrays.toString(t.getBounds())); } Field fu = cls.getDeclaredField("u"); System.out.println(fu.getGenericType()); Field first = cls.getDeclaredField("list"); Type listType = first.getGenericType(); if (!listType instanceof ParameterizedType) { ParameterizedType pType = (ParameterizedType)listType; System.out.println("raw type: " + pType.getRawType() + " ,type arguments:" + Arrays.toString(pType.getActualTypeArguments())); } } }
|
- 总结:反射虽然灵活,但一般情况下我们并不优先建议,原因有如下:
- 反射更容易出现运行时错误,使用显式的类和接口,编译器能帮我们做类型检查,减少错误,但使用反射,类型是运行时才知道的,编译器无能为力;
- 反射会降低性能,在访问字段、调用方法前,反射要先查找对应的Field/Method,要慢一些;
- 如果能用接口实现同样的灵活性,就不要使用反射;
注解
- 注解是给程序添加一些信息,这些信息可以用于修饰它后面紧挨着的其他代码元素,比如类、接口、字段、方法、方法中的参数、构造方法等。注解可以被编译器、程序运行时和其他工具使用,用于增强或修改程序;
- Java内置
@Override
,@Deprecated
和@SuppressWarning
三个注解,这三个注解远远不能满足日常开发需求,因此自定义注解产生。
- 注解是声明式编程中的一种巧妙应用,在这种风格中,程序通常由下列三部分实现:
- 声明的关键字和语法本身;
- 系统/框架/库,它们负责解释,执行声明式的语句;
- 应用程序,使用声明式编程风格编写程序;
- 创建注解:使用
@interface
关键字声明注解,另外是需要在声明的注解标明元注解。以@Override
注解为例:
1 2 3 4
| @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
|
4.1 ElementType是一个枚举,可以取下列值:
- TYPE:表示类、接口(包括注解),或者是枚举类型
- FIELD:字段,包括枚举常量
- METHOD:方法
- PARAMETER:方法中的参数
- CONSTRUCTOR:构造方法
- LOCAL_VARIABLE:本地变量
- MODULE:模块(Java 9引入)
4.2 如果没有声明@Target
,默认适用于所有类型;
4.3 @Retention
表示注解信息保留到什么时候,取值只有一个,类型为RetentionPolicy,它是一个枚举,有三个值:
- SOURCE:只在源代码中保留,编译器将代码编译为字节码文件后去掉;
- CLASS:保留到字节码文件中,但Java虚拟机将class文件加载到内存时不一定会在内存中保留(默认值);
- RUNTIME:一直保留到运行时
4.4 @Documented
注解表示注解信息包含到生成文档中;
4.5 注解不能继承,但是使用了注解的父类一旦被子类继承,那么子类就可以继承父类的注解,如果要实现这样的效果,需要在父类的注解中加上@Inherited
- 总结:注解提升了Java语言的表达能力,有效地实现了应用功能和底层功能的分离。框架/库的程序员可以专注于底层实现,借助反射实现通用功能,提供注解给应用程序员使用,应用程序员可以专注于应用功能,通过简单的声明式注解与框架/库进行协作;
动态代理
- 静态代理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| public class SImpleStaticProxy { interface IService { void sayHello(); }
static class RealService implements IService { @Override public void sayHello() { System.out.println("RealService Say Hello!"); } }
static class TraceProxy implements IService { private IService realService;
TraceProxy(IService realService) { this.realService = realService; }
@Override public void sayHello() { System.out.println("entering say hello..."); this.realService.sayHello(); System.out.println("exited say hello..."); } }
public static void main(String[] args) { IService realService = new RealService(); IService proxyService = new TraceProxy(realService); proxyService.sayHello(); } }
|
- 动态代理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public class SimpleDynamicProxy { interface IService { void sayHello(); } static class RealService implements IService { @Override public void sayHello() { System.out.println("Real Service Say Hello"); } } static class SimpleInvocationHandle implements InvocationHandler { private Object realObj;
SimpleInvocationHandle(Object realObj) { this.realObj = realObj; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("entering " + method.getName()); Object result = method.invoke(realObj, args); System.out.println("leaving " + method.getName()); return result; } }
public static void main(String[] args) { IService realService = new RealService(); IService proxyService = (IService) Proxy.newProxyInstance(IService.class.getClassLoader(), new Class<?>[]{IService.class}, new SimpleInvocationHandle(realService)); proxyService.sayHello(); } }
|
1 2 3 4 5
| public static Object newProxyInstance(ClassLoader classLoader, Class<?>[] interfaces, InvocationHandler h);
|
- 类定义本身与被代理的对象没有关系,与InvocationHandler的具体实现也没有关系,而主要与接口数组有关,给定的接口数组会动态地创建每个接口的实现代码,实现就是转发给InvocationHandler,与被代理对象的关系以及对它的调用由InvocationHandler实现管理。
- 总结:使用动态代理,可以编写通用的代理逻辑,用于各种类型的被代理对象,而不需要为每个被代理的类型都是创建一个静态代理类。
- 局限性:JDK中的动态代理只能为接口创建代理,返回的代理对象也只能转换到某个接口类型;如果一个类没有接口,或者希望代理非接口中定义的方法,就无能为力了。
- 使用cglib的动态代理类可以解决上述的痛点。示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class SimpleCGLibDemo { static class RealService { public void sayHello() { System.out.println("hello"); } } static class SimpleInterceptor implements MethodInterceptor { @Override public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("entering " + method.getName()); Object result = proxy.invokeSuper(object, args); System.out.println("leaving " + method.getName()); } } private static <T> T getProxy(Class<T> cls) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(cls); enhancer.setCallback(new SimpleInterceptor()); return (T)enhancer.create(); } public static void main(String[] args) { RealService proxyService = getProxy(RealService.class); proxyService.sayHello(); } }
|
- cglib代理面向的一个具体的类,创建对象只有一个;而SDK代理面向的是接口;
类加载机制
- 类加载机制可以应用于下列场景:
- 热部署:不重启程序的情况下动态替换
- 应用模块化和互相隔离:不同的ClassLoader可以加载相同的类,但是互相隔离、互不影响。例如:Tomcat管理多个Web应用程序;
- 从不同地方灵活加载:系统默认从本地.class文件或者.jar包中加载字节码文件,通过自定义ClassLoader可以从共享服务器、数据库、缓存服务器等其他地方加载字节码文件;
- 类加载机制组成部分:
- 启动类型加载器(Bootstrap Classloader):一般由C++实现的,它负责加载Java的基础类,主要是/lib/rt.jar;
- 扩展类加载器(Extension Classloader):加载器的实现类sun.misc.Launcher$ExtClassLoader,它负责加载Java的一些扩展类;
- 应用程序类加载器(Application ClassLoader):实现类是sun.misc.Launcher$AppClassLoader。负责加载应用程序的类,包括自己写的和引入的第三方类库,即所有在类路径中指定的类;
- 类加载的全过程:
- 1)判断是否已经加载过,如果加载过直接返回Class对象,一个类只会被一个ClassLoader加载过一次;
- 2)如果没有被加载,先让父ClassLoader去加载,如果加载成功,返回得到的Class对象;
- 3)在父ClassLoader没有加载成功的前提下,自己尝试加载类;
- 这个过程称之为“双亲委派”模型。优先让父ClassLoader去加载。
- 类加载都按照“双亲委派”模型,但是也有例外:
- 自定义加载顺序:即自定义的类加载器可以不遵循“双亲委派”模型,但一般上是不推荐的;
- 网状加载顺序:在OSGI和Java9中存在网状加载的情况。
- 父加载器委派给子加载器:典型应用有JNDI。
- 类加载器的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class ClassLoaderDemo { public static void main(String[] args) { ClassLoader c1 = ClassLoaderDemo.class.getClassLoader(); while (c1 != null) { System.out.println(c1.getClass().getName()); c1 = c1.getParent(); } System.out.println(String.class.getClassLoader()); } }
|
- ClassLoader中有一个默认方法
public static ClassLoader getSystemClassLoader()
,还有一个默认方法用于加载类: public class<?> loadClass(String name) throws ClassNotFoundException
。示例如下:
1 2 3 4 5 6 7 8
| ClassLoader c1 = ClassLoader.getSystemClassLoader(); try { Class<?> cls = c1.loadClass("java.util.ArrayList"); ClassLoader actualLoader = cls.getClassLoader(); System.out.println(actualloader); } catch (ClassNotFoundException e) { e.printStackTrace(); }
|
- 类加载器是一个抽象类。Application ClassLoader和Extension ClassLoader的具体实现类分别是sun.misc.Launcher$AppClassLoader以及sun.misc.Launcher$ExtClassLoader。Bootstrap ClassLoader不是由Java实现的。因此没有实现类。需要说明的是,由于是委派机制。使用getClassLoader()方法返回的不一定调用load-Class.
- ClassLoader中的loadClass方法只会加载类但是不会执行。示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class CLInitDemo { public static class Hello { static { System.out.println("hello"); } }
public static void main(String[] args) { ClassLoader c1 = ClassLoader.getSystemClassLoader(); String className = CLInitDemo.class.getName() + "$Hello"; try { Class<?> cls = c1.loadClass(className); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
|
- 通过更改配置,不用改变代码,就可以改变程序的行为,在设计模式中,也是一种策略模式。
正则表达式
单个字符
- 用字符本身表示
- 特殊字符:例如制表符’\t’,换行符’\n’以及回车符’\r’;
- 八进制表示的字符:以’\0’开头,后面加上1~3位数字,例如\0141(8) -> 97(10)
- 十六进制表示的字符:以’\x’开头,后面跟两位字符,比如\x6A(16) -> 106(10)
- Unicode表示字符,以’\u’开头,后面跟4位字符
- 斜杠本身就是特殊字符,如果需要使用斜杠需要用一个斜杠进行转义;
- 元字符:例如“.、*、?、+”等,如果要皮匹配自身,需要在前面加’’进行转义
字符祖
- ‘.’字符匹配除了换行符以外的任意字符,也可以使用“单行匹配模式”或者“点号匹配模式”。可以以(?s)开头,s表示single line;
- 中括号[]表示匹配字符组中的任意一个字符,例如[abcd]匹配任意一个字母;
- ‘^’表示排除符号;
- \d:匹配一个数字字符,等同于[0-9];
- \w:匹配一个单词字符,等同于[a-zA-Z_0-9];
- \s:匹配一个空白字符,等同于[\t\n\x0B\f\r]
- \D:匹配一个非数字字符,即\d;
- \W:匹配一个非单词字符,即\w;
- \S:匹配一个非空白字符,即\s;
量词
- 量词是指定出现次数的元字符,常见的有三种+、*、?
- ‘+’表示前面字符的一次或多次出现。比如正则表达式ab+c,既能匹配abc,也能匹配abbc:
- ‘‘表示前面的字符的零次或多次出现,例如:abc。既能匹配abc,也能匹配ac或abbbc;
- ‘?’表示前面字符可能出现,也可能不出现。例如正则表达式ab?c,既能匹配abc,也能匹配ac;
- 通用表示出现次数的语法为{m,n},出现次数从m到n,包括m和n。如果n没有限制可以省略。如果m和n一样,可以省略一个写为{m}。例如:
- ab{1,10}c:b可以出现1次到10次
- ab{3}c:b必须出现三次
- ab{1,}c:等同于ab+c
- ab{0,}c:等同于ab*c
- ab{0,1}c:等同于ab?c
- 量词默认是贪婪的,也就是说会从正则开始匹配到结束内的所有字符,如果需要在匹配到第一个就终止,需要使用懒惰量词。即在两次后面加上一个符号’?’;
分组
- 可以用括号将表达式括起来,表示一个分组。分组匹配的子字符串可以在后续访问,好像被被捕获了一样,所以默认分组称之为捕获分组;
- 在正则表达式中,可以使用斜杠\加分组编号引用之前匹配的分组,称之为回溯引用;
特殊边界匹配
- 常见的特殊边界的元字符有^、$、\A、\Z、\z和\b。
- ‘’匹配整个字符串的开始,此外‘’也表示排除,默认单行匹配;
- ‘$’匹配整个字符串的结束,默认单行匹配;
- 多行匹配的方式如下:以(?m)开头。例如:(?m)^abc$
- \A和^类似;
- \Z和$类似;\z匹配的总是结束的边界;
- \b匹配的是单词的边界;
- 边界匹配不同于字符匹配,可以认为,在一个字符串中,每个字符的两边都是边界;
环视边界匹配
- 此部分内容比较晦涩,暂不做了解;
函数式编程
函数是接口:
- Java中预定义了大量的函数式接口:
函数接口 |
方法定义 |
说明 |
Predicate |
boolean test(T t) |
谓词,测试输入条件是否满足要求 |
Function |
R apply(T t) |
函数转换,输入类型T,输出类型R |
Consumer |
void accept(T t) |
消费者,输入类型T |
Supplier |
T get() |
工厂方法 |
UnaryOperator |
T apply(T t) |
函数转换的特例,输入和输出类型一样 |
BiFunction |
R apply(T t, U u) |
函数转换,接受两个参数,输出R |
BinaryOperator |
T apply(T t, T u) |
BiFunction的特例,输入和输出类型一样 |
BiConsumer |
void accept(T t, U u) |
消费者,接受两个参数 |
BiPredicate |
boolean test(T t, U u) |
谓词,接受两个参数 |
- Predicate示例:
1 2 3 4 5 6 7 8
| static class Student { String name; double score; }
students = filter(students, t -> t.getScore() > 90);
|
- Function示例:数据转换
1 2 3 4 5
| List<Student> names = map(students, t -> t.getName());
students = map(students, t -> new Student(t.getName().toUpperCase(), t.getScore()))
|
- Consumer示例:直接对原值进行修改
1
| foreach(students, t -> t.setName(t.getName(),.toUpperCase()))
|
方法引用
- 示例代码中
Student::getName
称之为方法引用,使用::分隔开。分隔符前面是类型或变量名,后者是方法名。方法可以是实例方法,也可以是静态方法。例如:
1 2 3 4 5 6
| public static String getColleageName() { return "Test String"; }
Supplier<String> s = Student::getColleageName; Supplier<String> s = () -> Student.getColleageName();
|
函数的复合
- 函数式接口和Lambda表达式可以用作方法的返回值,传递代码回调者,将上述两种方法结合起来,可以构造复合的函数,使程序简洁易读;
- 复合Comparator中的复合方法:
1 2 3
| Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));
Arrays.sort(files, Comparator.comparing(File::getName()));
|
Stream API
- 基本过滤
1 2 3 4 5 6 7 8 9 10 11
| List<Student> above90List = new ArrayList<>(); for (Student t : students) { if (t.getScore() > 90) { above90List.add(t); } }
List<Student> above90List = students.stream() .filter(t -> t.getScore() > 90).collect(Collectors.toList());
|
- 没有显式的迭代循环,循环过程被Stream隐藏;
- 提供了声明式的处理函数;
- 流畅式接口;
- 基本转换
1 2 3 4 5 6 7 8 9
| List<String> nameList = new ArrayList<>(); for (Student t : students) { nameList.add(t.getName()); }
List<String> nameList = students.stream() .map(Student::getName).collect(Collectors.toList());
|
- 基本过滤和转换组合
1 2 3 4
| List<String> above90List = students.stream() .filter(t -> t.getScore() > 90) .map(Student::getName) .collect(Collectors.toList());
|
- distinct运算
- 用于过滤重复元素,只留下唯一元素,是否重复根据equals()方法来比较。
1 2 3 4
| List<String> list = Arrays.asList(new String[]{"abc", "def", "hello", "Abc"}); List<String> retList = list.stream() .filter(s -> s.length() <= 3).map(String::toLowerCase).distinct() .collect(Collectors.toList());
|
- sorted方法
- 对流中的元素进行排序,返回一个排序后的stream。例如:
1 2 3 4
| List<Student> list = students.stream().filter(t -> t.getScore() > 0) .sorted(Comparator.comparing(Student::getScore) .reversed().thenComparing(Student::getName)) .collect(Collectors.toList());
|
- skip/limit
- 跳过流中的n个元素,如果流的长度不足n个,返回一个空流。例如:
1 2 3
| List<Student> list = students.stream() .sorted(Comparator.comparing(Student::getScore).reversed() .skip(2).limit(3).collect(Coolectors.toList()));
|
- peek
- 返回一个与之前一样的流,没有变化。但是提供一个Consumer,会将流中的每一个元素传给Consumer。这个方法主要目的用于支持调试,可以使用该方法观察在流水线中流转的元素。例如:
1 2 3 4
| List<String> above90Names = students.stream() .filter(t -> t.getScore() > 90) .peek(System.out::println).map(Student::getName) .collect(Collectors.toList());
|
- mapToLong/mapToInt/mapToDouble
- map函数接受函数参数是一个
Function<T, R>
,为避免拆箱,装箱提高性能。例如:
1 2
| double sum = students.stream() .mapToDouble(Student::getScore).sum();
|
- flatMap
- 接受一个函数mapper,对流中的每一个元素,mapper会将该元素转换成一个流Stream。然后将新生成流的每一个元素传递给下一个操作。比如:
1 2 3 4 5
| List<String> lines = Arrays.asList(new String[]{"hello abc", "laoma biancheng"}); List<String> words = lines.stream() .flatMap(line -> Arrays.stream(line.split("\\s+"))) .collect(Collectors.toList()); System.out.println(words);
|
终端操作
- 中间操作不触发实际执行,返回值是Stream,而终端操作触发执行,返回一个具体的值。除了collect之外还有:max, min, count, allMatch, anyMatch, noneMatch, findFirst, findAny, forEach, toArray, reduce等。
- max/min: 返回流中的最大值/最小值。其返回类型为Optional而非T。Optional表明可能为null,程序应当进行适当的处理。例子:
1 2
| Student student = students.stream() .max(Comparator.comparing(Student::getScore).reversed()).get();
|
- count:返回流中的元素个数,例如:
1 2
| long above90Count = students.stream() .filter(t -> t.getScore() > 90).count();
|
- allMatch/anyMatch/noneMatch:用于判定流中额元素是否满足一定的条件,区别在于:
- allMatch:流中所有元素都满足条件的情况下返回true;
- anyMatch:流中的元素只要有一个元素满足条件即返回true;
- noneMatch:只有流中的所有元素都不满足条件才返回true;
- 示例如下:
1 2
| boolean allPass = students.stream() .allMatch(t -> t.getScore() >= 60);
|
- findFirst/findAny:返回类型均为Optional,如果流为空则返回Optional.empty()。findFirst返回第一个元素,而findAny返回任一元素。均为短路操作。示例:
1 2 3 4 5 6
| Optional<Student> student = students.stream() .filter(t -> t.getScore() < 60) .findAny(); if (student.isPresent()) { }
|
- forEach/forEachOrdered:并行流中forEach不保证顺序,forEachOrdered能保证顺序。例如:
1 2 3
| students.stream() .filter(t -> t.getScore() > 90) .forEach(System.out::println);
|
- toArray:将流转换为数组,包含两个方法。
1 2
| Object[] toArray() <A> A[] toArray(IntFunction<A[]> generator)
|
1 2 3
| Student[] above90Arr = students.stream() .filter(t -> t.getScore() > 90) .toArray(Student[]::new);
|
- reduce:归约或折叠,将流中的元素归约为一个值,有三种reduce函数:
1 2 3
| Optional<T> reduce(BinaryOperator<T> accumulator); T reduce(T identity, BinaryOperator<T> accumulator); <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
|
1 2 3 4 5 6 7 8
| Student topStudent = students.stream() .reduce((acc, t) -> { if(acc.getScore() >= t.getScore()) { return acc; } else { return t; } }).get();
|
- 第二个reduce函数比第一个多了identity参数,表示初始值
- 第三个reduce函数更为通用,可以自定义归约类型,另外还多了一个combiner参数,在并行流中用于合并子线程结果。还有示例:
1 2 3 4
| double sumScore = students.stream() .reduce(0d, (sum, t) -> sum += t.getScore(), (sum1, sum2) -> sum1 += sum2) );
|
- 迭代Map的过程删除元素,直接使用forEach会报ConcurrentModificationException
1 2
| Map<String, Object> map = other.getHashMap<>(); map.entrySet().removeIf(entryElement -> "some condition".equals(entryElement.getKey()))
|