Java拾遗补阙
初学Java,第一次就被它冗长的语句所震撼,略有接触之后,当初的疑问不仅没有减少,反而有指数增加趋势。上大学之前几乎没有学习过任何一门编程语言,刚接触的语言是C,从现在看来当初晦涩的C真是言简意赅、优美至极。进入信安后再一次接触到了PHP和Python,这两位都是较为出名的面向对象的编程语言,但是极端程度在Java面前还是不值一提
学Java的驱动力在于想要搞懂Java反序列化,萌生出这个念头的原因在于当时PHP反序列化学得还算舒适,第一次看Java反序列化的exp简直是在看天书,之后几天网上找了些Java入门教程,然而越是学习,问题也越为频繁地出现。前摇有点过长了,此处汇总了一些学习Java过程的疑惑在经过漫长思考后的不成熟的理解方式,可能内容会很割裂,但也无妨反正是我自己看:)
极端的面向对象
为什么说Java是非常极端的面向对象,因为直到我打字的这一刻,我还没有见到任何一行代码可以游离在类的外部执行,就连程序执行的入口main函数,也需要包装在一个类里面
================People类
package JavaStudy.greet;
public abstract class People { public abstract void greet(); }
class Chinese extends People { @Override public void greet() { System.out.println("你好"); } }
class American extends People { @Override public void greet() { System.out.println("Hello"); } }
class Korean extends People { @Override public void greet() { System.out.println("안녕하세요"); } }
================Main类
package JavaStudy.greet;
public class Main { public static void main(String[] args) { People Chinese = new Chinese(); People American = new American(); People Korean = new Korean();
Chinese.greet(); American.greet(); Korean.greet(); System.out.println(); People[] people = new People[3]; people[0]=Chinese; people[1]=American; people[2]=Korean; for(People element:people){ element.greet(); } } }
|
如果只能看到main函数的代码,几乎无法理解代码运行的逻辑,一般我们只能看到对象调用方法作为程序运行的主要方式,main函数此刻不再像C那么具体地负责每一个动作,而是一个调度器,协调多个对象调用它们的方法,并以此作为程序运行的单元
类型的声明与类型的转换
初学Java第一次看到实例化对象
而在PHP中,实例化一个对象长这样
Java多出了一个称为声明类型的概念,刚开始不能理解其中的含义,而且起初声明类型大多与实际类型匹配,直到后面学到继承,出现了下面的声明语句
其中Animal是Dog的父类,这里我们声明了一个Animal类型的对象,但它的实际类型却是Dog,而由此则出现了类型转化的问题。倘若Dog类有一个特别的bark方法,我们是否能直接调用animal.bark()?答案是不行,我们声明的类型是Animal,编译器会从Animal类中寻找bark,但是Animal类中并没有定义该方法,所以会编译报错。但是,这里的animal对象实际上就是Dog类实例化出来的,利用向下转型可以让animal成为Dog类型,语法如下
之后我们就可以愉快地使用dog.bark()了
但是这样的转型是由有风险的,如果animal是cat的实例化怎么办?所以这就是为什么可以看到在进行强转时都会有一层if判断
if (animal instanceof Dog) { Dog dog = (Dog) animal; dog.bark(); }
|
既然有向下转型,那也有向上转型,但是向上转型通常不需要特别去声明,因为父类的方法在子类本来就已经继承好了,完全可以直接使用
往往一个方法的返回值类型是确定的,比如返回的是通用的Object类型,但是我们若实际知道这是Dog类型且需要调用它,此时强转的重要性就更加凸显了
利用反射修改私有成员
利用反射我们可以在任何地方修改一个私有属性或私有方法,但使用它的代码也很复杂
定义一个Person类,里面放一些私有属性
public class Person { private String name; private int age;
public Person() { }
public Person(String name, int age) { this.name = name; this.age = age; }
public String getName() { return name; }
public int getAge() { return age; }
public void setName(String name) { this.name = name; }
private void testMethod() { System.out.println("Test Success!"); }
}
|
ReflectField类演示如何使用反射修改私有属性
import java.lang.reflect.Field; import java.lang.reflect.Modifier;
public class ReflectField { public static void main(String[] args) throws Exception { Person person = new Person("李四", 30); Class<?> clazz = person.getClass();
Field[] fields = clazz.getFields();
Field[] allFields = clazz.getDeclaredFields();
for (Field field : allFields) { System.out.println("字段名: " + field.getName()); System.out.println("字段类型: " + field.getType()); System.out.println("修饰符: " + Modifier.toString(field.getModifiers())); }
Field ageField = clazz.getDeclaredField("age"); ageField.setAccessible(true); int ageValue = (int) ageField.get(person); System.out.println("年龄: " + ageValue);
ageField.set(person, 13); System.out.println("修改后年龄: " + person.getAge()); } }
|
反射从Class<?> clazz = person.getClass();开始,这里获取了person对象运行时的类信息,利用getDeclaredField()获取字段,然后利用setAccessible()设置字段可访问,最后使用set(对象实例,修改后的值)方法实现对象字段的修改
可以注意到在获取修饰符时的代码明显更加复杂,因为修饰符本质上是一个int类型的位掩码(bitmask),在获取修饰符后还需要使用Modifier.toString()方法将其转化为可读的字符串
Field的get方法返回的是一个Object对象, int ageValue = (int) ageField.get(person)这一段进行了强转,也是成功照应上文
ReflectMethod类演示如何使用反射调用私有方法
import java.lang.reflect.Method;
public class ReflectMethod { public static void main(String[] args) throws Exception { Person person = new Person("张三", 28); Class<?> clazz = person.getClass();
Method[] methods = clazz.getDeclaredMethods();
Method setNameMethod = clazz.getMethod("setName", String.class); setNameMethod.invoke(person, "孙七");
Method getNameMethod = clazz.getMethod("getName"); String name = (String) getNameMethod.invoke(person); System.out.println("姓名: " + name);
Method privateMethod = clazz.getDeclaredMethod("testMethod"); privateMethod.setAccessible(true); privateMethod.invoke(person); } }
|
依旧从Class<?> clazz = person.getClass();出发,使用getMethod(方法名,参数类型数组)获得Method对象,使用setAccessible()获得调用权,之后对其可以使用invoke(对象实例,方法参数值数组)调用,若要调用的方法无参则可省略
相信都注意到到了String name = (String) getNameMethod.invoke(person);这里又来了一次强转,因为这里返回的又是一个Object对象(万金油了属于是)
应用
最近看题看到别人写的反射调用方法的方法,这里说说运行逻辑
default Object wagTail(Object input, String methodName, Class[] paramTypes, Object[] args) { try { Class cls = input.getClass(); Method method = cls.getMethod(methodName, paramTypes); return method.invoke(input, args); } catch (Exception e) { e.printStackTrace(); return null; } }
|
Class cls = input.getClass();,是不是很眼熟?这里就是前面说反射的开始,cls获取了input运行时类的信息,后面就是获得方法名、调用方法。原题用了一个几个Dog对象,实例化时的参数刚好能一一对应
Dog dog1 = setDog(Runtime.class,"getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}); Dog dog2 = setDog(Runtime.class,"invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}); Dog dog3 = setDog(Runtime.class,"exec", new Class[]{String.class}, new Object[]{"calc"});
|
原题对三个Dog对象进行了链式调用,这里的三个Dog对象其实只有第一个对象的Runtime.class被使用了,其他的都被循环覆盖掉了
public Object chainWagTail() { Object input = null;
for(Dog dog : this.dogs.values()) { if (input == null) { input = dog.object; }
Object result = dog.wagTail(input, dog.methodName, dog.paramTypes, dog.args); input = result; }
return input; }
|
第一次调用了Runtime.class.getMethod("getRuntime", null);,得到Runtime.getRuntime()方法的Method对象
第二次调用了getRuntime.invoke();,得到了一个Runtime实例
第三次调用了runtimeInstance.exec("calc");启动了计算器进程
序列化与反序列化
在Java中,序列化是将对象转化为二进制字节流输出的过程,反序列化则是其逆过程。若要让一个类具有序列化能力,需要调用Serialize接口,该接口不需要实现任何方法,用来标识该类可以进行序列恶化操作。在Java中,序列化和反序列化的可自定义程度是非常高的,不同的开发者都可以自己定义出一套独特的序列化逻辑
public class Serialize { public static String serialize(Object obj) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(obj); return Base64.getEncoder().encodeToString(baos.toByteArray()); } public static void deserialize(String base64Data) throws IOException,ClassNotFoundException { ByteArrayInputStream bais=new ByteArrayInputStream(Base64.getDecoder().decode(base64Data)); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); } }
|
在这里,进行序列化操作的实则只有oos.writeObject(obj);这一行代码,反序列化操作也只有ois.readObject();是在从字节流中读取对象。学习了一些Java后知道,由于writeObject()和readObject()
分别是ObjectOutputStream和ObjectInputStream类中具有的方法,所以第二行也不难理解。第一行的作用是创建内存缓冲区,这代表我们序列化的对象会被放入内存中存储,然后将内存中的数据进行编码后返回。除此以外,还可以将序列化后的数据写入文件或者进行压缩、加密等操作,自定义程度极高
private void writeObject(ObjectOutputStream oos) throws IOException { oos.defaultWriteObject(); oos.writeUTF(encrypt(this.password)); oos.writeLong(System.currentTimeMillis()); } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); this.password = decrypt(ois.readUTF()); long timestamp = ois.readLong(); System.out.println("序列化时间: " + new Date(timestamp)); }
|
serialVersionUID
Serialize接口的实现类都会计算出一个serialVersionUID常量,反序列化时计算出来的数值与JVM运行中类的值不一致会造成InvalidClassException异常,可以在要序列化的类中声明该值以避免报错