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第一次看到实例化对象

Dog dog = new Dog;
//格式为 声明类型 对象名称 = new 实际类型

而在PHP中,实例化一个对象长这样

$dog = new Dog;

Java多出了一个称为声明类型的概念,刚开始不能理解其中的含义,而且起初声明类型大多与实际类型匹配,直到后面学到继承,出现了下面的声明语句

Animal animal = new Dog;

其中AnimalDog的父类,这里我们声明了一个Animal类型的对象,但它的实际类型却是Dog,而由此则出现了类型转化的问题。倘若Dog类有一个特别的bark方法,我们是否能直接调用animal.bark()?答案是不行,我们声明的类型是Animal,编译器会从Animal类中寻找bark,但是Animal类中并没有定义该方法,所以会编译报错。但是,这里的animal对象实际上就是Dog类实例化出来的,利用向下转型可以让animal成为Dog类型,语法如下

Dog dog = (Dog) animal;

之后我们就可以愉快地使用dog.bark()

但是这样的转型是由有风险的,如果animalcat的实例化怎么办?所以这就是为什么可以看到在进行强转时都会有一层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();

// 获取public字段
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()方法将其转化为可读的字符串

Fieldget方法返回的是一个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()

分别是ObjectOutputStreamObjectInputStream类中具有的方法,所以第二行也不难理解。第一行的作用是创建内存缓冲区,这代表我们序列化的对象会被放入内存中存储,然后将内存中的数据进行编码后返回。除此以外,还可以将序列化后的数据写入文件或者进行压缩、加密等操作,自定义程度极高

// 假设要序列化的类中有transient字段
private void writeObject(ObjectOutputStream oos) throws IOException {
// 先执行默认序列化
oos.defaultWriteObject();
// 额外序列化transient字段(自定义加密)
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异常,可以在要序列化的类中声明该值以避免报错