java反序列

Java 序列化

Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。

将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。

整个过程都是 Java 虚拟机(JVM)独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。

类 ObjectInputStream 和 ObjectOutputStream 是高层次的数据流,它们包含反序列化和序列化对象的方法。

ObjectOutputStream 类包含很多写方法来写各种数据类型,但是一个特别的方法例外:

public final void writeObject(Object x) throws IOException

上面的方法序列化一个对象,并将它发送到输出流。相似的 ObjectInputStream 类包含如下反序列化一个对象的方法:

public final Object readObject() throws IOException,                                  ClassNotFoundException

该方法从流中取出下一个对象,并将对象反序列化。它的返回值为Object,因此,你需要将它转换成合适的数据类型。

为了演示序列化在Java中是怎样工作的,我将使用之前教程中提到的Employee类,假设我们定义了如下的Employee类,该类实现了Serializable 接口。

Employee.java 文件代码:

public class Employee implements java.io.Serializable {   
    public String name;   
    public String address;   
    public transient int SSN;   
    public int number;   
    public void mailCheck()   
    {      
        System.out.println("Mailing a check to " + name                           + " " + address);   } 
}

请注意,一个类的对象要想序列化成功,必须满足两个条件:

该类必须实现 java.io.Serializable 接口。

该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。

如果你想知道一个 Java 标准类是否是可序列化的,请查看该类的文档。检验一个类的实例是否能序列化十分简单, 只需要查看该类有没有实现 java.io.Serializable接口。

序列化对象

ObjectOutputStream 类用来序列化一个对象,如下的 SerializeDemo 例子实例化了一个 Employee 对象,并将该对象序列化到一个文件中。

该程序执行后,就创建了一个名为 employee.ser 文件。该程序没有任何输出,但是你可以通过代码研读来理解程序的作用。

注意: 当序列化一个对象到文件时, 按照 Java 的标准约定是给文件一个 .ser 扩展名。

SerializeDemo.java 文件代码:

import java.io.*;  public class SerializeDemo
{   
    public static void main(String [] args)   
    {      
        Employee e = new Employee();    
        e.name = "Reyan Ali";      
        e.address = "Phokka Kuan, Ambehta Peer";      
        e.SSN = 11122333;      
        e.number = 101;      
        try      
        {         
            FileOutputStream fileOut =         new FileOutputStream("/tmp/employee.ser");         ObjectOutputStream out = new ObjectOutputStream(fileOut);         
            out.writeObject(e);         
            out.close();         
            fileOut.close();         
            System.out.printf("Serialized data is saved in /tmp/employee.ser");      }catch(IOException i)      
        {          i.printStackTrace();      
        }   
    } 
}

反序列化对象

下面的 DeserializeDemo 程序实例了反序列化,/tmp/employee.ser 存储了 Employee 对象。

DeserializeDemo.java 文件代码:

import java.io.*;  
public class DeserializeDemo 
{   
    public static void main(String [] args)   
    {      
        Employee e = null;      
        try      
        {         
            FileInputStream fileIn = new FileInputStream("/tmp/employee.ser");         
            ObjectInputStream in = new ObjectInputStream(fileIn);         
            e = (Employee) in.readObject();         
            in.close();         
            fileIn.close();      
        }catch(IOException i)     
        {         
            i.printStackTrace();         
            return;      
        }catch(ClassNotFoundException c)      
        {         
            System.out.println("Employee class not found");         
            c.printStackTrace();         
            return;      }      
        System.out.println("Deserialized Employee...");      
        System.out.println("Name: " + e.name);     
        System.out.println("Address: " + e.address);      
        System.out.println("SSN: " + e.SSN);      
        System.out.println("Number: " + e.number);    
    } 
}

以上程序编译运行结果如下所示:

Deserialized Employee...
Name: Reyan Ali
Address:Phokka Kuan, Ambehta Peer
SSN: 0
Number:101

这里要注意以下要点:

readObject() 方法中的 try/catch代码块尝试捕获 ClassNotFoundException 异常。对于 JVM 可以反序列化对象,它必须是能够找到字节码的类。如果JVM在反序列化对象的过程中找不到该类,则抛出一个 ClassNotFoundException 异常。

注意,readObject() 方法的返回值被转化成 Employee 引用。

当对象被序列化时,属性 SSN 的值为 111222333,但是因为该属性是短暂的,该值没有被发送到输出流。所以反序列化后 Employee 对象的 SSN 属性为 0。

关于 java 中的序列化与反序列化

关于序列化,常又称为持久化,将其写入磁盘中。

进而对于编码规则来说:

任一一个实体类必须要去实现 Serializable 接口,方便以后将该类持久化,或者将其用于转为字节数组,用于网络传输。

对于一个实体类,不想将所有的属性都进行序列化,有专门的关键字 transient:

private transient String name;

当对该类序列化时,会自动忽略被 transient 修饰的属性。

关于 SerializableID

SerializableID 号是根据类的特征和类的签名算出来的。为什么 ID 号那么长,是因为为了避免重复。所以 Serializable 是给类加上 id 用的。用于判断类和对象是否是同一个版本。

如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值。原因是计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。

序列化流与反序列化流

ObjectOutputStream(序列化流)

ObjectOutputStream是序列化流,可以将Java程序中的对象写到文件中。

ObjectOutputStream 构造方法:

ObjectOutputStream(OutputStream out):参数要传递字节输出流。

ObjectOutputStream写对象的方法(特有方法):

void writeObject(Object obj): 向文件中写对象。

ObjectOutputStream 的使用步骤:

  • 创建序列化流,用来写。
  • 调用 writeObject 方法,写对象。
  • 释放资源。

tips: 要使用序列化流向文件中写的对象,必须实现 Serializable 接口。

public class Demo01ObjectOutputStream {
    public static void main(String[] args) throws IOException {
        //1. 创建序列化流,用来写
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("day12\\file03-obj.txt"));
        //2. 调用writeObject方法,写对象
        Person p = new Person("张三丰", 100);
        oos.writeObject(p);
        //3. 释放资源。
        oos.close();
    }
}

ObjectInputStream(反序列化流)

ObjectInputStream 是反序列化流, 可以将文件中的对象读取到 Java 程序中。

ObjectInputStream 的构造方法:

ObjectInputStream(InputStream in):参数要传递字节输入流。

ObjectInputStream 读取对象的方法(特有的方法):

Object readObject(): 从文件中读取对象,并将该对象返回。

反序列化流的使用步骤:

  • 创建 ObjectInputStream 反序列化流。
  • 调用 readObject 方法,读取对象。
  • 释放资源。

tips:调用 readObject 方法读取对象时,对象所对应的类不存在,那么会报错(ClassNotFoundException)

特殊情况:

被 static 修饰的成员变量无法序列化,无法写到文件。

如果不希望某个成员变量写到文件,同时又不希望使用 static 关键字, 那么可以使用 transient。transient 关键字表示瞬态,被 transient 修饰的成员变量无法被序列化。

public class Demo03StaticAndTransient {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        writePerson();
        readPerson();
    }

    //从文件中读取Person对象
    public static void readPerson() throws IOException, ClassNotFoundException {
        //创建反序列化流
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("day12\\file04-obj.txt"));
        //从文件中读取对象
        Object obj = ois.readObject();
        System.out.println(obj);
        //释放资源
        ois.close();
    }

    //向文件中写Person对象
    public static void writePerson() throws IOException {
        //创建序列化流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("day12\\file04-obj.txt"));
        //向文件中写Person对象
        oos.writeObject(new Person("张三丰", 100));
        //关流
        oos.close();
    }
}
public class Demo04SerialVersionUID {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //writePerson();
        readPerson();
    }

    //从文件中读取Person对象
    public static void readPerson() throws IOException, ClassNotFoundException {
        //创建反序列化流
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("day12\\file04-obj.txt"));
        //从文件中读取对象
        Object obj = ois.readObject();
        System.out.println(obj);
        //释放资源
        ois.close();
    }

    //向文件中写Person对象
    public static void writePerson() throws IOException {
        //创建序列化流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("day12\\file04-obj.txt"));
        //向文件中写Person对象
        oos.writeObject(new Person("张三丰", 100));
        //关流
        oos.close();
    }
}

例题

/*
    练习:
        1. 将存有多个Student对象的集合序列化操作,保存到list.txt 文件中。
        2. 反序列化list.txt ,并遍历集合,打印对象信息。

    步骤:
        1. 创建集合,用来保存Student
        2. 向集合中添加Student对象。
        3. 创建ObjectOutputStream序列化流,用来写。
        4. 调用writeObject方法,向文件中写集合对象。
        5. 释放资源。
        6. 创建ObjectInputStream反序列化流对象,用来读取
        7. 调用readObject方法,从文件中读取对象。
        8. 将读取到的集合进行遍历,并输出结果。

    注意:如果想要将多个对象保存在文件中,最好的一个方式可以将多个对象放入到一个集合中,然后直接将集合写到文件中。
 */
public class Demo05Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //1. 创建集合,用来保存Student
        List<Student> list = new ArrayList<>();
        //2. 向集合中添加Student对象。
        list.add(new Student("李云龙", 20));
        list.add(new Student("二营长", 22));
        list.add(new Student("秀琴", 25));
        //3. 创建ObjectOutputStream序列化流,用来写。
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("day12\\list.txt"));
        //4. 调用writeObject方法,向文件中写集合对象。
        oos.writeObject(list);
        //5. 释放资源。
        oos.close();
        //6. 创建ObjectInputStream反序列化流对象,用来读取
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("day12\\list.txt"));
        //7. 调用readObject方法,从文件中读取对象。
        List<Student> list2 = (List<Student>) ois.readObject();
        //8. 将读取到的集合进行遍历,并输出结果。
        for (Student stu : list2) {
            System.out.println(stu);
        }
    }
}

注意:

1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。

2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。

3)被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。

Java 泛型

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?

答案是可以使用 Java 泛型

使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。


泛型方法

你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。

下面是定义泛型方法的规则:

  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的 )。
  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。

java 中泛型标记符:

  • E - Element (在集合中使用,因为集合中存放的是元素)
  • T - Type(Java 类)
  • K - Key(键)
  • V - Value(值)
  • N - Number(数值类型)
  • - 表示不确定的 java 类型

实例

下面的例子演示了如何使用泛型方法打印不同类型的数组元素:

实例

public class GenericMethodTest 
{   // 泛型方法 printArray                            
    public static < E > void printArray( E[] inputArray )   
    {      // 输出数组元素                     
        for ( E element : inputArray ){                    System.out.printf( "%s ", element );         }         
        System.out.println();    
    }     
    public static void main( String args[] )    
    {        // 创建不同类型数组: 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 ); // 传递一个字符型数组    }  }

编译以上代码,运行结果如下所示:

整型数组元素为:
1 2 3 4 5 

双精度型数组元素为:
1.1 2.2 3.3 4.4 

字符型数组元素为:
H E L L O 

有界的类型参数:

可能有时候,你会想限制那些被允许传递到一个类型参数的类型种类范围。例如,一个操作数字的方法可能只希望接受Number或者Number子类的实例。这就是有界类型参数的目的。

要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界。

实例

下面的例子演示了"extends"如何使用在一般意义上的意思"extends"(类)或者"implements"(接口)。该例子中的泛型方法返回三个可比较对象的最大值。

实例

public class MaximumTest 
{   // 比较三个值并返回最大值   
    public static <T extends Comparable<T>> T maximum(T x, T y, T z)   {                           
        T max = x; // 假设x是初始最大值      
        if ( y.compareTo( max ) > 0 ){         
            max = y; //y 更大      
        }      
        if ( z.compareTo( max ) > 0 ){        
            max = z; // 现在 z 更大                 
        }      
        return max; // 返回最大对象   
    }   
    public static void main( String args[] )   
    {      
        System.out.printf( "%d, %d 和 %d 中最大的数为 %d\n\n",                   3, 4, 5, maximum( 3, 4, 5 ) );       
        System.out.printf( "%.1f, %.1f 和 %.1f 中最大的数为 %.1f\n\n",                   6.6, 8.8, 7.7, maximum( 6.6, 8.8, 7.7 ) );       
        System.out.printf( "%s, %s 和 %s 中最大的数为 %s\n","pear",         "apple", "orange", maximum( "pear", "apple", "orange" ) );   
    } 
}

编译以上代码,运行结果如下所示:

3, 4 和 5 中最大的数为 5

6.6, 8.8 和 7.7 中最大的数为 8.8

pear, apple 和 orange 中最大的数为 pear

泛型类

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。

和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

实例

如下实例演示了我们如何定义一个泛型类:

实例

public class Box<T> {     
    private T t;   
    public void add(T t) {    
        this.t = t;  
    }   
    public T get() {    
        return t;  
    }   
    public static void main(String[] args) {    
        Box<Integer> integerBox = new Box<Integer>();    
        Box<String> stringBox = new Box<String>();     
        integerBox.add(new Integer(10));   
        stringBox.add(new String("菜鸟教程"));     
        System.out.printf("整型值为 :%d\n\n", integerBox.get());    
        System.out.printf("字符串为 :%s\n", stringBox.get());  
    } 
}

编译以上代码,运行结果如下所示:

整型值为 :10

字符串为 :菜鸟教程

类型通配符

1、类型通配符一般是使用 ? 代替具体的类型参数。例如 List<?> 在逻辑上是 List,List 等所有 List<具体类型实参> 的父类。

实例

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);         getData(name);        getData(age);        getData(number);          }    public static void getData(List<?> data) {      System.out.println("data :" + data.get(0));   } }

输出结果为:

data :icon
data :18
data :314

解析: 因为 getData() 方法的参数是 List<?> 类型的,所以 name,age,number 都可以作为这个方法的实参,这就是通配符的作用。

2、类型通配符上限通过形如List来定义,如此定义就是通配符泛型值接受Number及其下层子类类型。

实例

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(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));       
    } 
}

输出结果:

data :18
data :314

解析://1 处会出现错误,因为 getUperNumber() 方法中的参数已经限定了参数泛型上限为 Number,所以泛型为 String 是不在这个范围之内,所以会报错。

3、类型通配符下限通过形如 List<? super Number> 来定义,表示类型只能接受 Number 及其上层父类类型,如 Object 类型的实例。

<? extends T>和<? super T>的区别

  • <? extends T>表示该通配符所代表的类型是T类型的子类。
  • <? super T>表示该通配符所代表的类型是T类型的父类。

对于泛型,只是允许程序员在编译时检测到非法的类型而已。

但是在运行期时,其中的泛型标志会变化为 Object 类型。

一个 List:

List<Integer> list = new ArrayList<>();

list.add(12);
//这里直接添加会报错
list.add("a");
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
//但是通过反射添加,是可以的
add.invoke(list, "kl");

System.out.println(list)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn) {
    return uniApplyStage(asyncPool, fn);
}

对于这个方法中的泛型应用,试着说下我的理解:

  • 首先函数式接口 Function<T,U> 中,只有一个抽象类方法U apply(T t) ;
  • 这里对传入的函数式表达式对入参类型T作了 super 限定,即必须为T类型或T类型的父类;
  • 对返回值类型 U 作了 extends 限定,即必须为U类型或U类型的子类;

例子还没想好,希望大神帮忙补充下。

在泛型中可以声明多个类型参数。为了指定两个或更多个类型参数,只需要使用逗号分隔参数列表即可。

如例带有两个类型参数的泛型:

package test;
//指定了两个类型参数:T和V,使用逗号将它们隔开。
public class Box<T, V> {

    private T obj1;
    private V obj2;
    
    public Box(T o1,V o2) {
        obj1 = o1;
        obj2 = o2;
    }
    public void showTypes() {
        System.out.println("Type of T is " + obj1.getClass().getName());
        System.out.println("Type of V is " + obj2.getClass().getName());
    }
    public T getObj1() {
        return obj1;
    }
    public V getObj2() {
        return obj2;
    }
  
    public static void main(String[] args) {
        
        //创建对象时必须为Box传递两个类型参数,这里Integer替换T,String替换V。
        Box<Integer,String> testObj1 = new Box<>(88,"Runoob");
        testObj1.showTypes();
        int t1 = testObj1.getObj1();
        System.out.println("value: " + t1);
        String v1 = testObj1.getObj2();
        System.out.println("value: " + v1);
        System.out.println("-----------------------------");
        
        //在这个例子中,尽管两个类型参数是不同的,但是可以将两个类型参数设置为相同的类型。这T,V都是String类型
        Box<String,String> testObj2 = new Box<>("Hello","Runoob");
        testObj2.showTypes();
        String t2 = testObj2.getObj1();
        System.out.println("value: " + t2);
        String v2 = testObj2.getObj2();
        System.out.println("value: " + v2);
    }
}

输出结果:

Type of T is java.lang.Integer
Type of V is java.lang.String
value: 88
value: Runoob
-----------------------------
Type of T is java.lang.String
Type of V is java.lang.String
value: Hello
value: Runoob

**注:**泛型的类型参数使用大写形式,且比较短,一般一个字母。在java库中,使用变量E表示集合的元素类型。K和V分别表示键与值的类型,比如:Map的键与值。T(需要时还可以用临近的字母U和S)表示“任意类型”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值