书接上文。
目录
前言
本文汇总一下杂七杂八的知识,重要程度会在文中表述。
一、反射
1、什么是反射?
在学golang时候接触过反射,但并没有看懂,这里温习一下。
现在给你一个类Object,要获取他的字段,方法,父类等信息,如何做呢?知道这个类是啥,求上述信息很简单,但万一不知道呢?在测试中可能就会出现这种情况。反射就是程序在运行期间,获取对象的各种信息的过程。换句话说,根据class实例,获取class各种信息的过程就是反射。
任何一个类class的实例都是由JVM动态加载实现的。JVM为每一个加载的class创建了对应的class实例,并管理所有信息包括类名、包名、字段、方法等。这里的动态加载是说JVM会在程序使用到某个class时候才会加载实现类。
2、反射的管理
如何获取一个类的实例呢?直接获取Class cls = String.class;实例变量获取String s="Hello";Class cls = s.getClass;类完整名获取Class cls = Class.forName("java.lang.String");
接着,如何获取类的字段呢?Field getField(name):根据字段名获取当前类和父类的字段信息;Field getDeclaredField(name):根据字段名获取当前类的字段信息;Field[] getFields():获取当前类和父类所有字段信息;Field[] getDeclaredFields():获取当前类的所有字段信息。
对于获取到的字段,使用f.getName(),f.getType()分别得到字段名字与类型。使用int m = f.getModifiers(),然后Modifier.isPublic(m)/isPrivate(m)可以实现对类型的判断。注意,要想获取当前类的私有字段,可以先设置f.setAccessible(true);再进行获取。不过JVM中安全检查的存在可能导致上述私有字段访问失效。设置字段值可以使用f.set(p,"...")进行设置。要注意的是,对字段读写会破坏字段的封装,谨慎使用。
接着如何获取类的方法?与字段是一样的思考方式。首先使用四个函数getMethod() getMethods() getDeclaredMethod() getDeclaredMethods()获取方法实例;对于获取的方法实例可以使用getName(),getReturnType(),getParameterTypes(),getModifiers(),可以使用Object invoke(Object instance, Object... parameters)来调用方法,setAccessible(true)来访问非公开方法。
如何获取类的构造方法呢?同样的思路,四个函数分别是getConstructor(),getConstructors(),getDeclaredConstructor(),getDeclaredConstructors();通过构造来创建实例newInstance();通过setAccessible(true)访问非公开构造方法。
如何获取继承关系?Class getSuperclass();实现当前类的所有接口使用Class[] getInterfaces()。
3、动态代理
所有接口类型不能实例化,如何不实现接口类,而是在运行期间创建某个实例呢?可以的,使用动态代理方法。过程:定义InvocationHandler实例,负责实现接口方法调用;Proxy.newProxyInstance()创建interface实例,三个参数是接口类class.getClassLoader、需要实现的接口new Class[]{Hello.class}、接口方法实例;强制转化Object为接口。调用接口即可。
这部分涉及代理模式,动态代理的作用网上的段子就解释得很好:我们通常使用class是直接定义,再装载到JVM上,运行时可以查到所有的信息,就像入学时每个人都需要登记一样;但是总有些情况,不需要知道类,也不能知道类的信息,打架雇人,就需要代理模式,仅仅实现接口——一堆操作的集合,不需要知道委托人的具体信息,只需要“时间地点”。这样来降低风险……
二、注解
Java又被戏称为“面向注解编程”,也是不无道理。注解用于简化开发,举个简单例子,@bean注解放在一个类的上方,在编译时会自动生成get\set方法,简化开发,Spring、Springboot中常会使用,这个建议大家在写业务代码时学习,不然背是背不住的。
三、泛型
这个是非常常见的概念了。当我们定义一个集合时,集合的元素可能是各种各样的类型,因此需要各种各样的集合类来实现。这样太麻烦了,于是乎,使用模板来表述同一类型但不同元素类型的集合。
使用泛型时,需要把泛型参数<T>转换成需要的class类型,List<String>list=new ArrayList<>();不指定泛型参数类型,只会转换成Object类型。JAVA中通过擦拭法实现泛型,导致编译器把类型<T>视为Object,根据<T>实现安全的强制转型。因此,擦拭法决定了:泛型不能是基本类型,不能获取带泛型类型的class,不能判断带泛型类型的类型,不能实例化泛型类型。
泛型的继承:一个类可以继承自泛型类,注意,<? extends Number>通配符作为方法参数时表示:调用Number的方法,只能读出,不能写入。<? super Integer>表示super通配符只能写不能读。
注意部分反射类型就是泛型,比如Class<T>,不这样做就只能强制转型。
四、集合
与大小固定的数组相比,集合能容纳更多类型的数据,能实时扩容,因此适用范围更广。这里只简单介绍常用类型与对应函数,需要大量做题才能更好领悟。
1、列表
链表有两种,一种是ArrayList,可自动扩容,实现增删改查,效率更高,List<String> list = new ArrayList<>();类似于golang中slice;另一种是LinkedList,即链表。
接口方法包括:在末尾添加一个元素add("a");在指定索引后加上元素add(0,"a");删除指定索引元素remove(0);删除某个元素remove("a")获取指定索引元素get(0);获取元素个数size()
快速创建List<Integer> list=List.of(1,2,5);
遍历for(Integer n:list)或者for(Iterator<Integer>it=list.iterator();it.hasNext();){Integer i=it.next();...}
列表与数组的转换:Integer[] array = list.toArray(Integer[]::new)
List<Integer> list = List.of(array);
注意:判断列表是否相等,一定是判断其中元素是否equals()!!!
2、哈希表
键值映射表:put放入元素,get获取元素,containsKey判断元素是否存在
遍历:for(String key:map.keySet())或者for(Map.Entry<String,Integer>entry : map.entrySet())
枚举类型map使用EnumMap,对于想要put顺序和输出顺序一致的情况,可以使用TreeMap!!!
配置Map——Properties:在配置文件中,常为String:String类型比如User:Tung,因此可以使用Properties,首先定义文件config.properties,然后创建实例Properties props = new Properties();load加载文件props.load(new java.io.FileInputStream(“config.properties”));getProperty("User")读取配置
Set只存储不重复的key,包含add\remove\contains等方法。
3、队列
先进先出的结构:获取长度使用size();注意!!!其余情况有两种标准——添加元素到队尾add/offer、获取队首元素并从队列中删除remove/poll、获取队首元素但不从队列中删除element/peek,前者一旦不成功会抛异常,后者只会返回FALSE!!!
优先队列:
队列中的元素是按照一定顺序排列的,Queue<User> q = new PriorityQueue<>(new UserComparator());比较方法为class UserComparator implements Comparator<User> {
public int compare(User u1, User u2) {...}...}
双端队列:顾名思义,两段都可以出入,因此需要加上LAST与FIRST——添加元素到队尾addLast/offerLast、添加元素到队首addFirst/offerFirst、取队尾元素并删除removeLast/pollLast、取队首元素并删除removeFirst/pollFirst 、取队尾元素不删除getLast/peekLast、取队首元素不删除getFirst/peekFirst。
4、栈
先进后出的结构,常用方法有push\pop\peek等
5、其他
如Collections,作用有创建空集合、单元素集合、不可变集合、排序等操作。一般都可以通过对应集合来实现,了解即可。
五、IO
1、常见的输入输出
我们在写ACM代码时候,需要手动输入输出,这里就简单介绍一下:
首先处理单个输入:
//输入两行数据,第一行是数组长度,第二行是数组元素
Scanner scanner = new Scanner(System.in);
int N = 100010;
int[] w = new int[N];
int n = scanner.nextInt();
for (int i = 0; i < n; i++) {
w[i] = scanner.nextInt();
}
//如果是字符串,使用next()/nextLine()
if (scan.hasNext()) {
String str1 = scan.next();
System.out.println("输入的数据为:" + str1);
}
//nextLine方法返回的是Enter键之前的所有字符,它是可以得到带空格的字符串的。
//next会自动消去有效字符前的空格,只返回输入的字符,不能得到带空格的字符串。
注意如果数据用,隔开1,2,3,4,5这种,需要用Scanner scanner = new Scanner(System.in).useDelimiter(",|\\s+");正则\\s+代表空格或换行符等等。
多组输入也是一样的,只不过多了循环罢了。
2、文件内容的输入输出
常用的两种方法,一种是字节流,一种是字符流。
//字节流,最好使用try(resource)保证流正确关闭
public void readFile() throws IOException {
InputStream input = new FileInputStream("src/readme.txt");
for (;;) {
int n = input.read();
if (n == -1) {
break;
}
System.out.println(n);
}
input.close();
}
public void writeFile() throws IOException {
OutputStream output = new FileOutputStream("out/readme.txt");
output.write(72);
}
//字符流,以char为单位
public void readFile() throws IOException {
Reader reader = new FileReader("src/readme.txt");
for (;;) {
int n = reader.read();
if (n == -1) {
break;
}
System.out.println((char)n);
}
reader.close();
}
public void writeFile() throws IOException {
Writer output = new FileWriter("out/readme.txt");
output.write(72);
}
六、异常
异常继承自Throwable,包含error、Exception两种。前者代表严重错误,程序无能为力——内存耗尽、栈溢出等;后者是运行时的错误,可以捕获处理。
Exception类有两大类,RuntimeException和其他(IOException等)。非RuntimeException必须强制捕获!!!其他可以不用强制捕获。
捕获时常使用try...catch:
try {
byte[] bs = toGBK("中文");
System.out.println(Arrays.toString(bs));
} catch (UnsupportedEncodingException e) {
System.out.println(e);
}
还可以在后面加上finally代表无论是否有异常都会执行的语句。
如何抛异常呢?throw new XXXException();
大型业务中,要学会定义异常类BaseException,继承自RuntimeException,然后其他异常继承这个BaseException。
最为常见的异常是NullPointerException,空指针(引用)异常。
最后说一下,业务中一般可以通过log文件排查错误。比如Log4j,有四个优先级debug、info、warning、error、fatal、off,有三大组件Logger输出不同日志级别、Appender输出指定地点、Layout输出一定格式。
定义static final Logger logger = Logger.getLogger(LogTest.class);
使用logger.debug("...");logger.error("...");
总结
这一部分的内容繁琐又枯燥,只能在实际业务开发、代码练习中加强学习。