Java其它特性


1 关于JDK、JRE、JVM和Java编译器及Java解释器

JDK是Java的开发工具包(SDK),提供了开发环境(编译器javac等工具,用于将java文件编译为class文件)以及运行环境JRE(JVM和Runtime辅助包,用于解析class文件使其得到运行)

JRE是Java运行环境,包含JVM和核心类库(的class文件)与支持文件(只要装了JRE就可以运行Java文件)

JVM:一种能够运行Java字节码(Java bytecode)的虚拟机。

字节码:字节码是已经经过编译,但与特定机器码无关,需要解释器转译后才能成为机器码的中间代码。

JVM:JVM有自己完善的硬件架构,如处理器、堆栈(Stack)、寄存器等,还具有相应的指令系统(字节码就是一种指令格式)。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需要生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM是Java平台无关的基础。JVM负责运行字节码:JVM把每一条要执行的字节码交给解释器,翻译成对应的机器码,然后由解释器执行。JVM解释执行字节码文件就是JVM操作Java解释器进行解释执行字节码文件的过程

Java编译器:将Java源文件(.java文件)编译成字节码文件(.class文件,是特殊的二进制文件,二进制字节码文件),这种字节码就是JVM的“机器语言”。javac.exe可以简单看成是Java编译器。

Java解释器:是JVM的一部分。Java解释器用来解释执行Java编译器编译后的程序。java.exe可以简单看成是Java解释器。

即时编译(Just-in-time compilation: JIT):又叫实时编译、及时编译。是指一种在运行时期把字节码编译成原生机器码的技术,一句一句翻译源代码,但是会将翻译过的代码缓存起来以降低性能耗损。这项技术是被用来改善虚拟机的性能的。

JIT编译器是JRE的一部分。原本的Java程序都是要经过解释执行的,其执行速度肯定比可执行的二进制字节码程序慢。为了提高执行速度,引入了JIT。在运行时,JIT会把翻译过来的机器码保存起来,以备下次使用。而如果JIT对每条字节码都进行编译,则会负担过重,所以,JIT只会对经常执行的字节码进行编译,如循环,高频度使用的方法等。它会以整个方法为单位,一次性将整个方法的字节码编译为本地机器码,然后直接运行编译后的机器码。

2 异常

异常处理可以使得程序处理费预期场景,并且继续正常的处理

在程序运行时,JVM如果检测出一个不可能执行的操作,就会出现运行时错误。Java中运行时错误会作为异常抛出

异常就是一种对象,表示阻止正常进行程序执行的错误或情况。如果异常没有被处理,那么程序会非正常终止。

异常是方法抛出的,方法的调用者可以捕获以及处理该异常,Java可以让一个方法抛出一个异常,该异常可以被调用者捕获和处理

如果某个方法不能按照正常的途径完成任务,就可以通过另一种路径退出方法。在这种情况下会抛出一个封装了错误信息的对象。此时,这个方法会立刻退出同时不返回任何值。另外,调用这个方法的其他代码也无法继续执行,异常处理机制会将代码执行交给异常处理器。

异常分类

Throwable 是 Java 语言中所有错误或异常的超类。下一层分为 Error 和 Exception

Error(错误)

Error 类是指Java运行时系统的内部错误和资源耗尽错误。是由JVM抛出的,应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。

Exception(RuntimeException、CheckedException)

RuntimeException(运行时异常)

如 : NullPointerException 、ClassCastException ; RuntimeException。是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。 描述的是程序设计的错误。

CheckedException(必检异常)

Error和RuntimeException统称为免检异常。Exception下的其他的都是必检异常(和 RuntimeExceptio同级)如 I/O错误导致的 IOException、QLException。

必检的意思是编译器会强制程序员检查,并通过try-catch块来进行处理,或者在方法头进行声明;而免检异常一般都是设计上的逻辑错误,Java不要求捕获声明免检异常

CheckedException一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行 try catch,该类异常一般包括几个方面:

  1. 试图在文件尾部读取数据

  2. 试图打开一个错误格式的 URL

  3. 试图根据给定的字符串查找 class 对象,而这个字符串表示的类并不存在

Java的异常处理模型基于三种操作:声明一个异常、抛出一个异常、捕获一个异常

(1)声明异常(方法头)

每个方法都要声明它可能抛出的必检异常的类型

在方法头中使用关键字 thows表明要抛出的异常种类(可以多个)

public void method() throws Exception1,Exception2,...

(2) 抛出异常(方法体)

检测到异常的程序可以创建一个合适的异常类型的实例并抛出它,要在方法体中使用thow对可能有异常的地方进行判断和抛出

thow new IllegalArgumentException("错误信息的描述字符串");//隐式创建异常的实例

一般来说,异常类有两个构造方法,一个是无参的,一个是可带描述信息的,该参数称为异常消息,可以通过getMessage()来获取异常的异常消息

(3)捕获异常(方法调用时(可以是一个方法的定义中调用了另一个方法))

使用try-catch块来捕获和处理异常,finally子句用来放置必然要执行的语句

try{
   statements
}catch(Exception1 ex1){
   handdler for ex1
}catch(Exception2 ex2){
   handdler for ex2
}
  ···
finally{
  ···
}

如果在执行try块的过程中没有出现异常,则跳过catch子句

*如果try块中的某条语句抛出一个异常,Java就会跳过剩余语句,然后开始查找处理这个异常的catch块,之后执行try-catch后的语句。处理这个异常的代码称作 异常处理器 *

当某个调用的方法抛出一个异常,从当前的方法开始,沿着方法调用链,按照异常的反向传播方向去寻找这个异常的处理器(某个catch块),如果异常没有在当前方法被捕获,就被传给该方法的调用者,这个过程一直重复(不断向上寻找),直到异常被捕获或者被传给main方法(如果最终没有捕获就会终止程序)

任何情况情况下,finally中的代码都会执行(即便之前有return也会执行,finally是在本层的最后执行的)

从一个通用父类可以派生出各种异常类,如果一个catch块可以捕获一个父类的异常对象,它就可以捕获那个父类的所有子类的异常对象

如果方法声明了一个必检异常(在方法头),那调用它的时候就必须加上try-catch块(在调用者这一层进行处理),或者在调用者的方法上声明同类异常(不在这一层处理,继续向上传导该异常)

void p1(){//在调用者这一层进行处理
  try{
    p2();
  }
  catch (IOException ex){
  ···
  }
}

void p1() throws IOException{//不在这一层处理,继续向上传导该异常
  p2();
}

对于捕获多个异常使用同样代码处理,可以用JDK7的 多捕获特征 来写(使用 |来分隔多个异常)

catch (Exception1 | Exception2 | ··· | Exceptionk ex){
  ···
}

从异常中获取信息

使用Throwable类中的方法

getMessage()获取描述该异常的信息

toString()返回三个字符串连接 异常类名 :getMessage()

printStackTrace() 打印toString()+getStackTrace

getStackTrace() 返回该异常堆对象相关堆栈信息

注意!使用异常需要 1 新建异常对象 2 从调用栈返回 3 沿方法调用链来传播异常以便找到异常处理器 这意味着花更长的时间和资源,所以要谨慎使用异常

重新抛出异常:如果在这一层处理不了这个异常,允许在catch中重新thow已经捕获异常,以便给本层后面的catch块或者上一层调用来处理

链式异常:同原始异常一起抛出一个新异常 比如thow new Exception("新异常的信息", 上一层原始异常ex)

创建自定义异常:可以通过派生 Exception类来定义一个自定义异常类

文本I/O:File类 绝对/相对路径 I/O方法(Scanner PrintWriter 类)try-with-resources自动关闭资源

Scanner&PrintWriter

3 Java反射

(1)反射概述

动态语言,是指程序在运行时可以改变其结构:新的函数可以引进,已有的函数可以被删除等结构上的变化。比如常见的 JavaScript 就是动态语言,除此之外 Ruby,Python 等也属于动态语言,而 C、C++则不属于动态语言。从反射角度说 JAVA 属于半动态语言。

在 Java 中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。

反射机制可以用来:

1 在运行时分析类的能力 。

2 在运行时查看对象。例如,编写一个 toString 方法供所有类使用。

3 实现通用的数组操作代码。

4 利用 Method 对象, 这个对象很像c++中的函数指针。

(2)Java反射API

反射 API 用来生成 JVM 中的类、接口或则对象的信息。

1. Class 类:反射的核心类,可以获取类的属性,方法等信息。(Java.lang.Class)

(以下都是Java.lang.reflec 包中的类)

2. Field 类 (域):表示类的成员变量,可以用来获取和设置类之中的属性值。

3. Method 类 (方法): 表示类的方法,它可以用来获取类中的方法信息或者执行方法。

4. Constructor 类 (构造器): 表示类的构造方法。

(3)反射使用步骤

  1. 获取想要操作的类的 Class 对象(3种方法),通过 Class 对象我们可以任意调用类的方法。

  2. 调用 Class 类中的方法(获取反射API对象),这是反射的使用阶段。

  3. 使用反射 API(Field Method Constructor的方法)来操作这些信息。

(4)Class类的使用

在程序运行期间, Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。 这个信息跟踪着每个对象所属的类。 虚拟机利用运行时类型信息选择相应的方法执行。保存这些信息的类被称为ClassObject 类中的 getClass( ) 方法将会返回一个 Class 类型的实例

一个 Class 对象将表示一个特定类的属性。

(4.1) 获取Class对象

E e = new E();
String s = e.getClass().getName();//得到某个实例对应类的类名

//获得Class类的3种方法
Class cl = e.getClass();//通过调用类的实例的getClass()方法获取
Class cl = E.class;//通过调用类的class属性来获取
Class cl = Class.forName("类的全路径(包名+类名)");//(最安全|性能最好)这是个静态方法,可以根据类名来得到对应的类的Class类的实例。注意!无论何时使用这个方法,都应该提供一个异常处理器

注意!一个 Class 对象实际上表示的是一个类型, 而这个类型未必一定是一种类。 例如, int 不是类, 但 int.class 是一个 Class 类型的对象。

Class类实际上是一个泛型类。例如,Employee.class的类型是Class<Employee>。但其实在大多数实际问题 中, 可以忽略类型参数, 而使用原始的Class 类

注意!鉴于历史原 getName 方法在应用于数组类型的时候会返回一个很奇怪的名字: Double[]class.getName()返回“ [Ljava.lang.Double;’’
int[].class.getName()返回“ [I”,

虚拟机为每个类型管理一个 Class 对象。(所以对于同一个类来说,其Class实例时唯一的,所有引用都指向一个地方) 因此, 可以利用 = 运算符实现两个类对象比较 的操作。

可以让启动变快的方法:首先, 显示一个启动画面; 然后,通过调用 Class.forName 手工地加载其他的类。(避免调用main方法加载所有的类花费大量时间)

(4.2)通过Class类型创建对应类的实例的两种方法

1.Class 对象的 newInstance()

使用 Class 对象的 newInstance()方法来创建该 Class 对象对应类的实例,但是这种方法要求该 Class 对象对应的类有默认的空构造器。(首先获得该类的Class对象,然后创建实例)

比如将 forName 与 newlnstance 配合起来使用, 可以根据存储在字符串中的类名创建一个对象

e.getClass().newlnstance();

String s = "java.util.Random";
Object m = Class.forName(s).newlnstance();//注意使用时要类型转换

2.调用 Constructor 对象的 newInstance()

先使用 Class 对象获取指定的 Constructor 对象,再调用 Constructor 对象的 newInstance()方法来创建 Class 对象对应类的实例,通过这种方法可以选定构造方法创建实例

//获取 Person 类的 Class 对象
Class clazz = Class.forName("reflection.Person"); 
//使用.newInstane 方法创建对象
Person p = (Person) clazz.newInstance();

//获取构造方法并创建对象(构造方法参数是对应类的Class对象)
Constructor c = clazz.getDeclaredConstructor(String.class,String.class,int.class);
//创建对象并设置属性
Person p1 = (Person) c.newInstance("李四","男",20);

(4.3)调用Class的方法(得到对应的反射API实例)

Class类中的getFields、getMethods和getConstructors方法将分别返回类提供的 public域、方法和构造器数组,其中包括超类的公有成员。

Class 类的 getDeclareFields、getDeclareMethods 和 getDeclaredConstructors 方法将分别返回类中声明的全部域、方法和构造器, 其中包括私有和受保护成员,但不包括超类的成员。

(5)反射API的使用(Field Method Constructor)(用于检查类的结构)

这三个类都有一个叫做 getName 的方法,用来返回项目的名称。

Method类有一 个 getType 方法,用来返回描述域所属类型的 Class 对象。

Method 和 Constructor 类有能够报告参数类型的方法, Method 类还有一个可以报告返回类型的方法。

这3个类还有一个叫 做 getModifiers 的方法,它将返回一个整型数值,用不同的位开关描述 public 和 static 这样 的修饰符使用状况。

另外, 还可以利用 java.lang.reflect 包中的 Modifier 类的静态方法分析 getModifiers 返回的整型数值。 例如, 可以使用 Modifier 类中的 isPublic、 isPrivate 或 isFinal 判断方法或构造器是否是 public、 private 或 final。 我们需要做的全部工作就是调用 Modifier 类的相应方法, 并对返回的整型数值进行分析, 另外,还可以利用 Modifier.toString 方法将修饰符打印出来。

(5.1) 运行时分析反射对象(查看修改数据域内容 Field类)

在编写程序时, 如果知道想要査看的域名 和类型, 查看指定的域是一件很容易的事情。而利用反射机制可以查看在编译时还不清楚的对象域。

(a)得到Field对象

  1. 使用getDeclaredFields方法来获得所有成员的一个Field对象数组,然后在进行查找

  2. 使用Class类的getField方法根据表示域名的字符串,返回一个 Field对象(要使用这个必须加上try-catch块

(b)查看和修改Field对象

查看和修改对象域的关键方法是 Field 类中的 get和set方法。 如果 f 是一个 Field 类型的对象(例如,通过 getDeclaredFields 得到的对象) obj 是某个包含 f 域的类的对象, f.get(obj) 将返回一个对象,其值为 obj 域的当前值

当然,可以获得就可以设置 调用 f.set(obj value) 可以将 obj 对象的 f 域设置成新值

Employee harry = new Employee("Harry Hacker", 35000, 10, 1, 1989);
Class cl = harry.getClass();// 拿到了Employee的Class
Field f = cl.getDeclaredField("name")://拿到了该类的指定成员的Filed对象
Object v = f.get(harry);//使用该Field实例去用get方法获取某一个类的实例的指定成员的内容,并返回一个新的变量实例(注意是Object类型 要转换)
// the value of the name field of the harry object, i .e., the String object "Harry Hacker"

实际上,这段代码存在一个问题。由于 name 是一个私有域, 所以 get 方法将会抛出一个 IllegalAccessException。 只有利用 get 方法才能得到可访问域的值。 除非拥有访问权限, 否则 Java 安全机制只允许査看任意对象有哪些域, 而不允许读取它们的值。(必须要加上try-catch块)

Field类使用还有很多内容,比如 对于基本类型获取,get方法可以自动打包,setAccessible 方法,编写通用 toString 方法**

(5.2) 调用任意方法(Method类)

在 C 和 C++ 中, 可以从函数指针执行任意函数。从表面上看, Java 没有提供方法指针, 即将一个方法的存储地址传给另外一个方法, 以便第二个方法能够随后调用它。事实上, Java 的设计者曾说过: 方法指针是很危险的, 并且常常会带来隐患。 他们认为 Java 提供的接口 (interface )是一种更好的解决方案。 然而,反射机制允许你调用任意方法

(a)得到Method对象

  1. 可以通过调用 getDeclareMethods 方法, 然后对返回的 Method 对象数组进行查找, 直到发现想要的方法为止。

  2. 也可以通过调用 Class 类中的 getMethod 方法得到想要的方法。因为有可能存在若干个相同名字的方法,所以还必须提供想要的方法的参数类型。(必须要加上try-catch块)

Method getMethod(String name, Class... parameterTypes)

Method ml = Employee.class.getMethod("getName");
Method m2 = Employee.class.getMethod("raiseSalary", double.class); 

(b)调用Method对象中的方法

在 Method 类中有一个 invoke 方法, 它允许调用包装在当前 Method 对象中的方法。

invoke方法的签名是 Object invoke(Object obj, Object... args)

第一个参数是隐式参数, 其余的对象提供了显式参数

对于静态方法, 第一个参数可以被忽略, 即可以将它设置为 null

假设用 ml 代表 Employee 类的 getName 方法(是一个Method对象),下面这条语句显示了如何调用这个方法:

String n = (String) ml.invoke(harry);

invoke 的参数和返回值必须是 Object 类型的。这就意味着必须进行多次的类型转换。 这样做将会使编译器错过检查代码的机会。 因此,等到测试阶段才会发现这些错误,找 到并改正它们将会更加困难。不仅如此,使用反射获得方法指针的代码要比仅仅直接调用方法明显慢一些。

有鉴于此, 建议仅在必要的时候才使用 Method 对象,而最好使用接口以及 Java SE 8 中 的 lambda 表达式

建议 Java 开发者不要使用 Method 对象的回调功能。 使用接口进行回调会使得代码的执行速度更快, 更易于维护。(?不明白这是什么意思)

4 包 类路径 注释 注解

Java 允许使用包 (package)将类组织起来。 借助于包可以方便地组织自己的代码, 并将 自己的代码与别人提供的代码库分开管理。

标准的 Java 类库分布在多个包中, 包括 java.lang、java.util 和 java.net 等。 标准的 Java 包具有一个层次结构。 如同硬盘的目录嵌套一样, 也可以使用嵌套层次组织包。 所有标准的 Java 包都处于 java 和 javax 包层次中。

一个类可以使用所属包中的所有类, 以及其他包中的公有类(public class。) 我们可以 采用两种方式访问另一个包中的公有类。 第一种方式是在每个类名之前添加完整的包名(很麻烦)。

java.tiie.LocalDate today = java.tine.Local Date.now() ;

更简单且更常用的方式是使用 import 语句。 import 语句是一种引用包含 在包中的类的简明描述。 一旦使用了 import 语句, 在使用类时, 就不必写出包的全名了。

可以使用 import 语句导人一个特定的类或者整个包。 import 语句应该位于源文件的顶部 ( 但位于 package 语句的后面 )。

import java.util.*;

LocalDate today = LocalDate.now();

在 C++中, 与 包 机 制 类 似 的 是 命 名 空 间(namespace)。 在 Java 中, package 与 import 语句类似于 C++中的 namespace 和 using 指令。

静态导入

import 语句不仅可以导入类, 还增加了导人静态方法和静态域的功能。(调用静态域和方法时更方便)

import static java.lang.System.*;

out.println("Goodbye, World!"); // i.e., System
e., System.exit exit(9); //i.

要想将一个类放人包中, 就必须将包的名字放在源文件的开头, 包中定义类的代码之前。

package com.horstiann.corejava;

public class Employee
{
}

如果没有在源文件中放置 package 语句, 这个源文件中的类就被放置在一个默认包 ( defaulf package ) 中。 默认包是一个没有名字的包。

类路径

类存储在文件系统的子目录中。 类的路径必须与包名匹配。

另外, 类文件也可以存储在 JAR(Java 归档 )文件中。 在一个 JAR 文件中, 可以包含 多个压缩形式的类文件和子目录, 这样既可以节省又可以改善性能。 在程序中用到第三方 ( third-party ) 的库文件时, 通常会给出一个或多个需要包含的 JAR 文件。JDK 也提供了许多 的 JAR 文件

JAR 文件使用 ZIP 格式组织文件和子目录。可以使用所有 ZIP 实用程序查看内部 的 rt.jar 以及其他的 JAR 文件。

文档注释

JDK 包含一个很有用的工具, 叫做 javadoc, 它可以由源文件生成一个 HTML 文档。

如果在源代码中添加以专用的定界符 /** 开始的注释, 那么可以很容易地生成一个看上 去具有专业水准的文档。这是一种很好的方式, 因为这种方式可以将代码与注释保存在一个 地方。如果将文档存人一个独立的文件中, 就有可能会随着时间的推移, 出现代码和注释不 一致的问题。 然而, 由于文档注释与源代码在同一个文件中, 在修改源代码的同时, 重新运 行 javadoc 就可以轻而易举地保持两者的一致性。

@author @param 等等

Java注解

Annotation(注解)是 Java 提供的一种对元程序中元素关联信息和元数据(metadata)(关于数据的数据)的途径和方法。Annatation(注解)是一个接口,程序可以通过反射来获取指定程序中元素的 Annotation对象,然后通过该 Annotation 对象来获取注解中的元数据信息

元注解的作用是负责注解其他注解。 Java5.0 定义了 4 个标准的 meta-annotation 类型,它们被用来提供对其它 annotation 类型作说明。

4种元注解(负责注解其他注解)

@Target 修饰的对象范围

@Target 说明了 Annotation 所修饰的对象范围: Annotation 可被用于 packages、types(类、

接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数

和本地变量(如循环变量、catch 参数)。在 Annotation 类型的声明中使用了 target 可更加明晰

其修饰的目标

@Retention 定义 被保留的时间长短

Retention 定义了该 Annotation 被保留的时间长短:表示需要在什么级别保存注解信息,用于描

述注解的生命周期(即:被描述的注解在什么范围内有效),取值(RetentionPoicy)由:

SOURCE:在源文件中有效(即源文件保留)

CLASS:在 class 文件中有效(即 class 保留)

RUNTIME:在运行时有效(即运行时保留)

@Documented 􏰁描述javadoc

@ Documented 用于描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API,因此可以被例如 javadoc 此类的工具文档化。

@Inherited 阐述了某个被标注的类型是被继承的

@Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的 annotation 类型被用于一个 class,则这个 annotation 将被用于该class 的子类。

3种标准注解

@override 函数重写

@Deprecated 对不应该再使用的方法做注释

@SuppressWarnings 关闭特定警告信息(有参数)

注解处理器

如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了。使用注解的过程中,很重要的一部分就是创建和使用注解处理器(利用元注解)

/1:*** 定义注解*/ 
  @Target(ElementType.FIELD) 
  @Retention(RetentionPolicy.RUNTIME)
  @Documented
public @interface FruitProvider {
/**供应商编号*/
public int id() default -1; /*** 供应商名称*/
public String name() default "";
/** * 供应商地址*/
public String address() default "";
}
//2:注解使用
public class Apple {
@FruitProvider(id = 1, name = "陕西红富士集团", address = "陕西省西安市延安路")
private String appleProvider;
public void setAppleProvider(String appleProvider) {
this.appleProvider = appleProvider; }
public String getAppleProvider() { return appleProvider;
}
}
/3:*********** 注解处理器 ***************/
public class FruitInfoUtil {
public static void getFruitInfo(Class<?> clazz) {
String strFruitProvicer = "供应商信息:";
Field[] fields = clazz.getDeclaredFields();//通过反射获取处理注解
for (Field field : fields) {
if (field.isAnnotationPresent(FruitProvider.class)) {
FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class); //注解信息的处理地方
strFruitProvicer = " 供应商编号:" + fruitProvider.id() + " 供应商名称:"
+ fruitProvider.name() + " 供应商地址:"+ fruitProvider.address();
System.out.println(strFruitProvicer); }
} }
 }
 public class FruitRun {
public static void main(String[] args) {
FruitInfoUtil.getFruitInfo(Apple.class); /***********输出结果***************/
// 供应商编号:1 供应商名称:陕西红富士集团 供应商地址:陕西省西安市延 }
}

5 日志 断言 调试

6 Java序列化(创建可复用的Java对象)

Java 平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比 JVM 的生命周期更长。但在现实应用中,就可能要求在 JVM 停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java 对象序列化就能够帮助我们实现该功能。

使用 Java 对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量

除了在持久化对象时会用到对象序列化之外,当使用 RMI(远程方法调用),或在网络中传递对象时,都会用到对象序列化。Java 序列化 API 为处理对象序列化提供了一个标准机制,该 API 简单易用。

Serializable 实现序列化

在 Java 中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化。

通过 ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化。

在类中增加 writeObject 和 readObject 方法可以实现自定义序列化策略。

序列化ID

虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)

Transient 关键字阻止该变量被序列化到文件中

在变量声明前加上Transient 关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。



Java基础

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!