首页 > 其他 > 详细

Java explore

时间:2014-03-11 13:27:52      阅读:460      评论:0      收藏:0      [点我收藏+]
1.Java字节代码的操纵
    在一般的Java应用开发过程中,开发人员使用Java的方式比较简单。打开惯用的IDE,编写Java源代码,再利用IDE提供的功能直接运行Java 程序就可以了。这种开发模式背后的过程是:开发人员编写的是Java源代码文件(.java),IDE会负责调用Java的编译器把Java源代码编译成平台无关的字节代码(byte code),以类文件的形式保存在
磁盘上(.class)。Java虚拟机(JVM)会负责把Java字节代码加载并执行。Java通过这种方式来实现其  “编写一次,到处运行(Write once, run anywhere)” 的目标。Java类文件中包含的字节代码可以被不同平台上的JVM所使用。Java字节代码不仅可以以文件形式存在于磁盘上,也可以通过网络方式来下载,  还可以只存在于内存中。JVM
中的类加载器会负责从包含字节代码的字节数组(byte[])中定义出Java类。在某些情况下,可能会需要动态的生成  Java字节代码,或是对已有的Java字节代码进行修改。

1.1动态编译Java源文件:
    在一般情况下,开发人员都是在程序运行之前就编写完成了全部的Java源代码并且成功编译。对有些应用来说,Java源代码的内容在运行时刻才能确定。这个时候就需要动态编译源代码来生成Java字节代码,再由JVM来加载执行。Java源代码的内容在运行时刻才能确定。

    可以使用JDK6自带的API、com.sum.tools.java.Main、Eclipse JDT Core提供的编译器、


1.2Java字节代码增强:
    在Java字节代码生成之后,对其进行修改,增强其功能。
    (相当于对应用程序的二进制文件进行修改

    Java字节代码增强通常与Java源文件中的注解(annotation)一块使用。
    (注解在Java源代码中声明需要增强的行为及相关的元数据据,由框架在运行时刻完成对字节代码增强)

    可以用在:集中减少冗余代码和对开发人员屏蔽底层的实现细节上。
    (JavaBeans中的getter/setter,通过字节代码增强,只需要声明Bean中的属性即可,getter/setter方法可以通过修改字节代码来自动添加。)
    (JPA,在调试程序的时候,会发现实体类中被添加了一些额外的域或方法。这些内容是在运行时刻由JPA实现动态添加的。)
    (面向方面编程AOP中也有使用。)

    Java类或接口的字节代码组织形式:
bubuko.com,布布扣

    使用类库:ASM、cglib、serp、BCEL等。

    对类文件进行增强的时机是需要在Java源代码编译之后,在JVM执行之前:
  • 由IDE在完成编译操作之后执行。如Google App Engine 的Eclipse 插件会在编译之后运行DataNucleus 来实现对实体类进行增强。
  • 在构建过程中完成,比如通过Ant 或 Maven 来执行相关操作。
  • 实现自己的类加载器。当获取到Java 类的字节代码之后,先进行增强处理,再从修改过的字节代码中定义出Java类。
  • 通过JDK5 引入的Java.lang.instrument 包来完成

1.3 java.lang.instrument 基本思路:
        在JVM 启动时候添加一些代理(agent)。每个代理是一个jar 包,其清单(manifest)文件中会指定一个代        理类。这个类包含一个permain 方法。JVM 在启动的时候会首先执行代理类的premain 方法,在执行Java 程序本身main 方法。在premian 方法中就可以对程序本身的字节代码进行修改。

    JDK6 中还允许JVM 启动后进行动态代理。

    (java.lang.instrument 支持两种修改场景,一种是重定义一个Java类,即完全替代一个Java 类的字节码;另一种是转换已有的Java 类,相当于前面提到的类字节代码增强。)

1.4总结:
    Java字节码:可以很容易的对二进制分发的Java 程序进行修改,非常适合于性能分析、调试跟踪和日记记录等任务。另外一个非常重要的作用是把开发人员从繁琐的Java 语法中解放出来。开发人员应该只需要负责编写与业务逻辑相关的重要代码。对于那些只是因为语法要求而添加的,或是模式固定的代码,完全可以将其字节代码动态生成出来。字节代码增强和源代码生成是不同的概念。 源代码生成之后,就已经成为了程序的一部分,开发人员需要去维护它:要么手工修改生成出来的源代码,要么重新生成。而字节代码的增强过程,对于开发人员是完全透明的。
    

2.Java类的加载、链接和初始化
    Java 字节码的表现是字节数组(byte[]),而Java 类在JVM中表现是 java.lang.Class类的对象。
    
2.1Java 类加载器
    Java 类的加载是由类加载器来完成的。

    类加载器分成两类:
    启动类加载器(bootstrap)和用户自定义加载器(user-defined)。
    两者的区别在于启动类加载器是由JVM 的原生代码实现的,而用户自定义的类加载器都是继承java.lang.ClassLoader 类。

    类加载器需要完成的最终功能是定义一个Java 类,即把java 字节码转换成JVM 中的java.lang.Class类对象。

    层次组织结构和代理模式。
    (由于代理模式的存在,启动一个类的加载过程的类加载器和最终定义这个类的类加载器可能并不是一个,前者称为初始化类加载器,后者称为定义类加载器。两者的关联在于:一个Java 类的定义类加载器是该类所导入的其它Java 类的初始化类加载器。比如类A 通过import 导入类B ,那么由类A 的定义加载器负责启动类B 的加载器过程。)

    一般类加载器会先代理给其父类加载器,当父类加载器找不到的时候,才会自己加载。
    (这个逻辑封装在java.lang.ClassLoader 类的 loadClass()方法中。)    

    Tomcat为每个Web应用都提供一个独立的类加载器,使用的就是自己优先加载策略。

    类加载器的一个重要用途是在JVM中为相同名称的Java类创建隔离空间。
    在JVM中,判断两个类是否相同,不仅根据该类的二进制名称,还需要根据两个类的定义类加载
    (为两个类定义加载的同一个类的两个对象之间赋值:java.lang.ClassCastException)

2.2Java 类的链接
    Java类的链接指的是将Java 类的二进制代码合并到JVM 运行状态之中的过程。
    (在链接之前,这个类必须被成功加载。)

    类的链接包括:验证、准备、解析。

    验证:确保Java 类的二进制表示在结构上完全正确。(Java.lang.VerifyError)
    准备:创建Java类中的静态域,并设置默认值。(不会执行代码。)
    解析:确保(在一个Java 类中会包含对其它类或接口的形式引用、包括它的父类、所实现的接口、方法形式参数和返回值Java类等。)这些被引用的类能被正确的找到。(解析过程可能会导致其它的Java 类被加载)

    两种解析策略:
    一种做法是在链接时候,就递归把所有依赖的形式引用都进行解析。
    另一种则是只在一个形式引用真正需要的时候才进行解析。(Oracle的JDK 6采用的是这种
    (Java类只是被引用了,但是并没有被真正用到,那么这个类有可能就不被解析)

2.3Java类的初始化
    当一个Java类第一次被真正使用到的时候,JVM 会进行该类的初始化操作。
    (执行静态代码块和初始化静态域,会按照源代码从上到下顺序执行静态代码块和初始化静态域。
    (一个类被初始化前,它的直接父类也需要被初始化。但是一个接口的初始化,不会引起父接口的初始化。)
   
    Java类和接口初始化条件:
  • 创建一个Java类实例:MyClass obj = new MyClass();
  • 调用一个Java类中的静态方法:MyClass.sayHello();
  • 给Java 类或结构中声明的今天域赋值:MyClass.value = 10;
  • 访问Java类或接口中声明的静态域,并且该域不是常值变量:int value = MyClass.value();
  • 在顶层Java类中执行assert 语句。
    (通过Java反射API 也可能造成类和接口被初始化,需要注意的是:当访问一个Java类或接口中的静态域的时候,只有真正声明这个域的类或接口才会被初始化。)

2.4创建自己的类加载器
    典型场景包括实现特定的Java字节代码查找方式、对字节代码进行加密/ 机密以及实现同名 Java类的隔离。

    只需要继承java.lang.ClassLoader 类并覆盖写对应的方法即可。

  • defineClass():这个方法用来完成从Java字节代码的字节数组到java.lang.Class的转换。这个方法是不能被覆写的,一般是用原生代码来实现的。
  • findLoadedClass():这个方法用来根据名称查找已经加载过的Java类。一个类加载器不会重复加载同一名称的类。
  • findClass():这个方法用来根据名称查找并加载Java类。(覆盖,自定义)
  • loadClass():这个方法用来根据名称加载Java类。
  • resolveClass():这个方法用来链接一个Java类

3.Java线程:基本概念、可见性与同步
    应用的例子:高性能Web服务器、游戏服务器、搜索引擎爬虫等。
    (需要同时处理成千上万个请求,一般采用多线程或事件驱动架构)

3.1Java线程的基本概念
    进程(process)和线程(thread)

    操作系统中进程是资源的组织单位。进程有一个包含了程序内容和数据的地址空间,以及其它的资源,包括打开的文件、子进程和信号处理器等。不同进程的地址空间是互相隔离的。

    线程表示程序执行流程,是CPU调度的基本单位。线程有自己的程序计数器、寄存器、栈和帧等。
 
3.2可见性 
    可见性(visibility)的问题是Java多线程应用中的错误根源。
  • CPU 内部的缓存:现在的CPU一般都拥有层次结构的几级缓存。CPU直接操作的是缓存中的数据,并在需要的时候把缓存中的数据与主存进行同步。因此在某些时刻,缓存中的数据与主存内的数据可能是不一致的。某个线程所执行的写入操作的新值可能当前还保存在CPU的缓存中,还没有被写回到主存中。这个时候,另外一个线程的读取操作读取的就还是主存中的旧值。
  • CPU的指令执行顺序:在某些时候,CPU可能改变指令的执行顺序。这有可能导致一个线程过早的看到另外一个线程的写入操作完成之后的新值。
  • 编译器代码重排:出于性能优化的目的,编译器可能在编译的时候对生成的目标代码进行重新排列。

    Java内存模型(Java Memory Model)就是为了实现“编写一次,到处运行”为目的而引用的。
    (描述了程序中共享变量的关系以及在主存中写入和读取这些变量值的底层细节)

    Java内存模型定义了Java语言中的synchronized、volatile和final等关键词对主存中变量读写操作的意义。Java开发人员使用这些关键词来描述程序所期望的行为,而编译器和JVM负责保证生成的代码在运行时刻的行为符合内存模型的描述。
    比如对声明为volatile的变量来说,在读取之前,JVM会确保CPU中缓存的值首先会失效,重新从主存中进行读取;而写入之后,新的值会被马上写入到主存中。

    Java内存模型中一个重要的概念是定义了“在之前发生(happens-before)”的顺序。
    (如果一个动作按照“在之前发生”的顺序发生在另外一个动作之前,那么前一个动作的结果在多线程的情况下对于后一个动作就是肯定可见的。

    最常见的“在之前发生”的顺序包括:对一个对象上的监视器的解锁操作肯定发生在下一个对同一个监视器的加锁操作之前;对声明为volatile的变量的写操作肯定发生在后续的读操作之前。

    有了“在之前发生”顺序,多线程程序在运行时刻的行为在关键部分上就是可预测的了。
    (编译器和JVM会确保“在之前发生”顺序可以得到保证。)

    如果一个变量的值可能被多个线程读取,又能被最少一个线程锁写入,同时这些读写操作之间并没有定义好的“在之前发生”的顺序的话,那么在这个变量上就存在数据竞争(data race)。
    数据竞争的存在是Java多线程应用中要解决的首要问题。决的办法就是通过synchronized和volatile关键词来定义好“在之前发生”顺序。)

3.3Java中的锁
    当数据竞争存在的时候,最简单的解决办法就是加锁。锁机制限制在同一时间只允许一个线程访问产生竞争的数据的临界区。
    (一个线程可以在一个Java对象上加多次锁。同时JVM保证了在获取锁之前和释放锁之后,变量的值是与主存中的内容同步的。

3.4Java线程的同步    
    Java提供的线程之间的等待-通知机制。
    (当线程所要求的条件不满足时,就进入等待状态;而另外的线程则负责在合适的时机发出通知来唤醒等待中的线程。Java中的java.lang.Object类中的wait/notify/notifyAll 方法组就是完成线程之间的同步的。

    在某个Java对象上面调用wait方法的时候,首先要检查当前线程是否获取到了这个对象上的锁。如果没有的话,就会直接抛出java.lang.IllegalMonitorStateException异常。如果有锁的话,就把当前线程添加到对象的等待集合中,并释放其所拥有的锁。当前线程被阻塞,无法继续执行,直到被从对象的等待集合中移除。引起某个线程从对象的等待集合中移除的原因有很多:对象上的notify方法被调用时,该线程被选中;对象上的notifyAll方法被调用;线程被中断;对于有超时限制的wait操作,当超过时间限制时;JVM内部实现在非正常情况下的操作。
    
    :wait/notify/notifyAll操作需要放在synchronized代码块或方法中,这样才能保证在执行  wait/notify/notifyAll的时候,当前线程已经获得了所需要的锁。当对于某个对象的等待集合中的线程数目没有把握的时候,最好使用  notifyAll而不是notify。notifyAll虽然会导致线程在没有必要的情况下被唤醒而产生性能影响,但是在使用上更加简单一些。由于线程可能在非正常情况下被意外唤醒,一般需要把wait操作放在一个循环中,并检查所要求的逻辑条件是否满足。
    典型的使用模式如下所示:    
private Object lock = new Object();
synchronized (lock) {
while (/* 逻辑条件不满足的时候 */) {  
try {
    lock.wait();
} catch (InterruptedException e) {}
}
//处理逻辑
上述代码中使用了一个私有对象lock 来作为加锁的对象,其好处是可以避免其它代码错误的使用这个对象。

3.5中断线程
    通过一个线程对象的interrupt()方  法可以向该线程发出一个中断请求。中断请求是一种线程之间的协作方式。

    当线程A通过调用线程B的interrupt()方法来发出中断请求的时候,线程A 是在请求线程B的注意。线程B应该在方便的时候来处理这个中断请求,当然这不是必须的。

    当中断发生的时候,线程对象中会有一个标记来记录当前的中断状态。通过isInterrupted()方法可以判断是否有中断请求发生。

    如果当中断请求发生的时候,线程正处于阻塞状态,那么这个中断请求会导致该线程退出阻塞状态。

    可能造成线程处于阻塞状态的情况有:当线程通过调用wait()方法进入一个对象的等待集合中,或是通过sleep()方法来暂时休眠,或是通过join()方法来等待另外一个线程完成的时候。
    (在线程阻塞的情况下,当中断发生的时候,会抛出java.lang.InterruptedException,  代码会进入相应的异常处理逻辑之中。

    实际上在调用wait/sleep/join方法的时候,是必须捕获这个异常的。中断一个正在某个对象的等待集合中的线程,会使得这个线程从等待集合中被移除,使得它可以在再次获得锁之后,继续执行java.lang.InterruptedException异常的处理逻辑。

    通过中断线程可以实现可取消的任务。在任务的执行过程中可以定期检查当前线程的中断标记,如果线程收到了中断请求,那么就可以终止这个任务的执行。当遇到java.lang.InterruptedException 的异常,不要捕获了之后不做任何处理。如果不想在这个层次上处理这个异常,就把异常重新抛出。当一个在阻塞状态的线程被中断并且抛出java.lang.InterruptedException 异常的时候,其对象中的中断状态标记会被清空。如果捕获了java.lang.InterruptedException 异常但是又不能重新抛出的话,需要通过再次调用interrupt()方法来重新设置这个标记。

4.Java垃圾回收机制与引用类型
    Java语言的一个重要特性是引入了自动的内存管理机制

4.1Java垃圾回收机制
    Java的垃圾回收器要负责完成3件任务:分配内存、确保被引用的对象的内存不被错误回收以及回收不再被引用的对象的内存空间。

    般情况下,当垃圾回收器在进行回收操作的时候,整个应用的执行是被暂时中止(stop-the-world)的。
    (这是因为垃圾回收器需要更新应用中所有对象引用的实际内存地址。

    不同的硬件平台所能支持的垃圾回收方式也不同。
    (多CPU的平台上,就可以通过并行的方式来回收垃圾。而单CPU平台则只能串行进行。)

    服务器端应用可能希望在应用的整个运行时间中,花在垃圾回收上的时间总数越小越好。
    而对于与用户交互的应用来说,则可能希望所垃圾回收所带来的应用停顿的时间间隔越小越好。

    Java 垃圾回收机制最基本的做法是分代回收。
{
    内存中的区域被划分成不同的世代,对象根据其存活的时间被保存在对应世代的区域中。

    一般的实现是划分成3个世代: 年轻、年老和永久。
  •     内存的分配是发生在年轻世代中的。
  •     当一个对象存活时间足够长的时候,它就会被复制到年老世代中。
    对于不同的世代可以使用不同的垃圾回收算法
    
    年轻世代的内存区域被进一步划分成伊甸园(Eden)和两个存活区(survivor space)。
  • 伊甸园是进行内存分配的地方,是一块连续的空闲内存区域。在上面进行内存分配速度非常快,因为不需要进行可用内存块的查找。
  • 两个存活区中始终有一个是空白的。
 
    在进行垃圾回收的时候,伊甸园和其中一个非空存活区中还存活的对象根据其存活时间被复制到当前空白的存活区或年老世代中。经过这一次的复制之后,之前非空的存活区中包含了当前还存活的对象,而伊甸园和另一个存活区中的内容已经不再需要了,只需要简单地把这两个区域清空即可。下一次垃圾回收的时候,这两个存活区的角色就发生了交换。一般来说,年轻世代区域较小,而且大部分对象都已经不再存活,因此在其中查找存活对象的效率较高。

    而对于年老和永久世代的内存区域,则采用的是不同的回收算法,称为“标记-清除-压缩(Mark-Sweep-Compact)”。标记的过程是找出当前还存活的对象,并进行标记;清除则遍历整个内存区域,找出其中需要进行回收的区域;而压缩则把存活对象的内存移动到整个内存区域的一端,使得另一端是一块连续的空闲区域,方便进行内存分配和复制。
}

    JDK 5 中提供了4种不同的垃圾回收机制。
    串行回收方式、分代回收、并行回收方式、并发标记-清除回收。
  • 最常用的是串行回收方式,即使用单个CPU回收年轻和年老世代的内存。在回收的过程中,应用程序被暂时中止。回收方式使用的是上面提到的最基本的分代回收。串行回收方式适合于一般的单CPU桌面平台。
  • 如果是多CPU的平台,则适合的是并行回收方式。这种方式在对年轻世代  进行回收的时候,会使用多个CPU来并行处理,可以提升回收的性能。
  • 并发标记-清除回收方式适合于对应用的响应时间要求比较  高的情况,即需要减少垃圾回收所带来的应用暂时中止的时间。这种做法的优点在于可以在应用运行的同时标记存活对象与回收垃圾,而只需要暂时中止应用比较短的时间。

    通过JDK中提供的JConsole可以很容易的查看当前应用的内存使用情况。在JVM启动的时候添加参数  -verbose:gc  可以查看垃圾回收器的运行结果。

4.2Java引用类型
    如果一个内存中的对象没有任何引用的话,就说明这个对象已经不再被使用了,从而可以成为被垃圾回收的候选。
    (有引用存在的对象超出JVM中的内存总数:OutOfMemory)

    强引用:
    在一般的Java程序中,见到最多的就是强引用(strong reference)。如Date date = new Date(),date就是一个对象的强引用。对象的强引用可以在程序中到处传递。强引用的存在限制了对象在内存中的存活时间。

    软引用:
    软引用(soft reference)在强度上弱于强引用,通过类SoftReference来表示。它的作用是告诉垃圾回收器,程序中的哪些对象是不那么重要,当内存不足的时候是可以被暂时回收的。
    (软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。

    弱引用:
    弱引用(weak reference)在强度上弱于软引用,通过类WeakReference来  表示。它的作用是引用一个对象,但是并不阻止该对象被回收。
    (弱引用的作用在于解决强引用所带来的对象之间在存活时间上的耦合关系。弱引用最常见的用处是在集合类中,尤其在哈希表中。哈希表的接口允许使用任何Java对象作为键来使用。当一个键值对被放入到哈希表中之后,哈希表  对象本身就有了对这些键和值对象的引用。如果这种引用是强引用的话,那么只要哈希表对象本身还存活,其中所包含的键和值对象是不会被回收的。如果某个存活  时间很长的哈希表中包含的键值对很多,最终就有可能消耗掉JVM中全部的内存。对于这种情况的解决办法就是使用弱引用来引用这些对象,这样哈希表中的键和值对象都能被垃圾回收。

    幽灵引用:
    (Java提供的对象终止化机制(finalization)。 在Object类里面有个finalize方法,其设计的初衷是在一个对象被真正回收之前,可以用来执行一些清理的工作。因为Java并没有提供类似C++的析构函数一样的机制,就通过finalize方法来实现。但是问题在于垃圾回收器的运行时间是不固定的,所以这些清理工作的实际运行时间也是不能预知的。幽灵引用(phantom reference)可以解决这个问题。
    在创建幽灵引用PhantomReference的时候必须要指定一个引用队列。当一个对象的finalize方法已经被调用了之后,这个对象的幽灵引用会被加入到队列中。通过检查该队列里面的内容就知道一个对象是不是已经准备要被回收了。
    (移动设备:程序可以在确定一个对象要被回收之后,再申请内存创建新的对象。

    引用队列:
    在有些情况下,程序会需要在一个对象的可达到性发生变化的时候得到通知。
    (比如某个对象的强引用都已经不存在了,只剩下软引用或是弱引用。但是还需要对引用本身做一些的处理。)
    (典型的情景是在哈希表中。引用对象是作为WeakHashMap中的键对象的,当其引用的实际对象被垃圾回收之后,就需要把该键值对从哈希表中删除。
    有了引用队列(ReferenceQueue),就可以方便的获取到这些弱引用对象,将它们从表中删除。在软引用和弱引用对象被添加到队列之前,其对实际对象的引用会被自动清空。通过引用队列的poll/remove方法就可以分别以非阻塞和阻塞的方式获取队列中的引用对象。
    

5.Java泛型
    Java泛型(generics)是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter)。声明的类型参数在使用时用具体的类型来替换。
    (型最主要的应用是在JDK 5中的新集合类框架中。
    (一个方法如果接收List<Object>作为形式参数,那么如果尝试将一个List<String>的对象作为实际参数传进去,却发现无法通过编译。
    
5.1类型擦除(type erasure)
    Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。

    (而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的  地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。

很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:
  • 泛型类并没有自己独有的Class类对象。比如并不存在List<String>.class 或是List<Integer>.class,而只有List.class。
  • 静态变量是被泛型类的所有实例所共享的。对于声明为MyClass<T>的类,访问其中的静态变量的方法仍然是  MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象,都是共享一个静态变量。
  • 泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM 是无法区分两个异常类型MyException<String>和MyException<Integer>的。对于JVM 来说,它们都是MyException类型的。也就无法执行与异常对应的catch语句。

5.2通配符与上下界
    如List<?>就声明了List中包含的元素类型是未知的。  通配符所代表的其实是一组类型,但具体的类型是未知的。
List<?>所声明的就是所有类型都是可以的。但是List<?>并不等同于List<Object>。

    因为对于List<?>中的元素只能用Object来引用,在有些情况下不是很方便。在这些情况下,可以使用上下界来限制未知类型的范围。

    List<? extends Number>说明List中可能包含的元素类型是Number及其子类。

    List<? super Number>则说明List中包含的是Number及其父类。

5.3系统类型
    根据Liskov替换原则,子类是可以替换父类的。但是反过来的话,即用父类的引用替换子类引用  的时候,就需要进行强制类型转换。

    引入泛型之后的类型系统增加了两个维度:
    一个是类型参数自身的继承体系结构
    对于  List<String>和List<Object>这样的情况,类型参数String是继承自Object的。

    另外一个是泛型类或接口自身的继承体系结构。
    (List接口继承自Collection接口。
  • 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即List<String> 是  Collection<String>  的子类型,List<String> 可以替换Collection<String>。这种情况也适用于带有上下界的类型声明。
  • 当泛型类的类型声明中使用了通配符的时候,其子类型可以在两个维度上分别展开。如对Collection<? extends Number>来说,其子类型可以在Collection这个维度上展开,即List<? extends Number>和Set<? extends Number>等;也可以在Number这个层次上展开,即Collection<Double>和  Collection<Integer>等。如此循环下去,ArrayList<Long>和  HashSet<Double>等也都算是Collection<? extends Number>的子类型。
  • 如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。
    (List<Object>改成List<?>。List<String>是List<?>的子类型,因此传递参数时不会发生错误。 


5.4开发自己的泛型类
    泛型类与一般的Java类基本相同,只是在类和接口定义上多出来了用<>声明的类型参数。一个类可以有多个类型参数,如  MyClass<X, Y, Z>。  每个类型参数在声明的时候可以指定上界。所声明的类型参数在Java类中可以像一般的类型一样作为方法的参数和返回值,或是作为域和局部变量的类型。
    (但是由于类型擦除机制,类型参数并不能用来创建对象或是作为静态变量的类型。考虑下面的泛型类中的正确和错误的用法。
class ClassTest<X extends Number, Y, Z> { 
    private X x
    private static Y y; //编译错误,不能用在静态变量中
    public X getFirst() {
        //正确用法 
        return x
    } 
    public void wrong() { 
        Z z = new Z(); //编译错误,不能创建对象
    }
}  

5.5最佳实践
  • 在代码中避免泛型类和原始类型的混用。比如List<String>和List不应该共同使用。这样会产生一些编译器警告和潜在的运行时异常。当需要利用JDK 5之前开发的遗留代码,而不得不这么做时,也尽可能的隔离相关的代码。
  • 在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。
  • 泛型类最好不要同数组一块使用。你只能创建new List<?>[10]这样的数组,无法创建new List<String>[10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用集合类即可。
  • 不要忽视编译器给出的警告信息。 

6.Java 注解
    JDK 5中引入了源代码中的注解(annotation)这一机制。注解使得Java源代码中不但可以包含功能性的实现
代码,还可以添加元数据。注解的功能类似于代码中的注释,所不同的是注解不是提供代码功能的说明,而是实现程序功能的重要组成部分。Java注解已经在很多框架中得到了广泛的使用,用来简化程序中的配置。

6.1使用注解
    在一般的Java开发中,最常接触到的可能就是@Override@Supp?ressWarnings这 两个注解了。

    使用@Override的时候只需要一个简单的声明即可。这种称为标记注解(marker annotation ),它的出现就代表了某种配置语义。而其它的注解是可以有自己的配置参数的。配置参数以名值对的方式出现。

    使用@SupressWarnings的时候需要类似@SupressWarnings({"uncheck", "unused"})这样的语法。
    在括号里面的是该注解可供配置的值。由于这个注解只有一个配置参数,该参数的名称默认为value,并且可以省略。
    而花括号则表示是数组类型。
    (在JPA中的@Table注解使用类似@Table(name = "Customer", schema = "APP")这样的语法。从这里可以看到名值对的用法。在使用注解时候的配置参数的值必须是编译时刻的常量。

    从某种角度来说,可以把注解看成是一个XML元素,该元素可以有不同的预定义的属性。而属性的值是可以在声明该元素的时候自行指定的。在代码中使用注解,就相当于把一部分元数据从XML文件移到了代码本身之中,在一个地方管理和维护。

6.2开发注解
    通过该注解可以在源代码中记录每个类或接口的分工和进度情况。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Assignment {
    String assignee();
    int effort();
    double finished() default 0;
}
@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。
(方法的名称就是参数的名称,返回值类型就是参数的类型。可以通过default来声明参数的默认值。)

在这里可以看到@Retention和@Target这样的元注解,用来声明注解本身的行为。
@Retention用来声明注解的保留策略,有CLASS、RUNTIME和SOURCE这三种,分别表示注解保存在类文件、JVM运行时刻和源代码中。
只有当声明为RUNTIME的时候,才能够在运行时刻通过反射API来获取到注解的信息。@Target用来声明注解可以被添加在哪些类型的元素上,如类型、方法和域等。

6.3处理注解
    JDK 5中提供了apt工具用来对注解进行处理。apt是一个命令行工具,与之配套的还有一套用来描述程序语义结构的Mirror API。Mirror API(com.sun.mirror.*)描述的是程序在编译时刻的静态结构。通过Mirror API可以获取到被注解的Java类型元素的信息,从而提供相应的处理逻辑。

    体的处理工作交给apt工具来完成。编写注解处理器的核心是AnnotationProcessorFactoryAnnotationProcessor两个接口。
(后者表示的是注解处理器,而前者则是为某些注解类型创建注解处理器的工厂。)


7.Java反射与动态代理
    通过反射API可以获取程序在运行时刻的内部结构。反射API中提供的动态代理也是非常强大的功能,可以原生实现AOP中的方法拦截功能。知道了Java类的内部结构之后,就可以与它进行交互,包括创建新的对象和调用对象中的方法等。这种交互方式与直接在源代码中使用的效果是相同的,但是又额外提供了运行时刻的灵活性。

7.1基本用法
    Java反射API的第一个主要作用是获取程序在运行时刻的内部结构。

    只要有了java.lang.Class类的对象,就可以通过其中的方法来获取到该类中的构造方法、域和方法。对应的方法分别getConstructor、getField和getMethod。
    这三个方法还有相应的getDeclaredXXX本,区别在于getDeclaredXXX版本的方法只会获取该类自身所声明的元素,而不会考虑继承下来的。
    (Constructor、Field和Method这三个类分别表示类中的构造方法、域和方法。这些类中的方法可以获取到所对应结构的元数据

    反射API的另外一个作用是在运行时刻对一个Java对象进行操作。
    
    这些操作包括动态创建一个Java类的对象,获取某个域的值以及调用某个方法。在Java源代码中编写的对类和对象的操作,都可以在运行时刻通过反射API来实现。

    一个简单的Java类。
class MyClass {
    public int count;
    public MyClass(int start) {
        count = start;
    }
    public void increase(int step) {
        count = count + step;
    }
}

MyClass myClass = new MyClass(0); // 一般做法
myClass.increase(2);
System.out.println("Normal -> " + myClass.count);
try {
    // 获取构造方法
    Constructor constructor = MyClass.class.getConstructor(int.class);
    // 创建对象
    MyClass myClassReflect = constructor.newInstance(10);
    // 获取方法
    Method method = MyClass.class.getMethod("increase"int.class);
    // 调用方法
    method.invoke(myClassReflect, 5);
    // 获取域
    Field field = MyClass.class.getField("count");
    // 获取域的值
    System.out.println("Reflect -> " + field.getInt(myClassReflect));
catch (Exception e) {
    e.printStackTrace();
}

    由于数组的特殊性,Array类提供了一系列的静态方法用来创建数组和对数组中的元素进行访问和操作。
Object array = Array.newInstance(String.class, 10); //等价于 new String[10]
Array.set(array, 0, "Hello"); //等价于array[0] = "Hello"
Array.set(array, 1, "World"); //等价于array[1] = "World"
System.out.println(Array.get(array, 0)); //等价于array[0] 

    使用Java反射API的时候可以绕过Java默认的访问控制检查,比如可以直接获取到对象的私有域的值或是调用私有方法。只需要在获取到Constructor、Field和Method类的对象之后,调用setAccessible方法并设为true即可。

7.2处理泛型
比如在代码中声明了一个域是List<String>类型的,虽然在运行时刻其类型会变成原始类型List,但是仍然可以通过反射来获取到所用的实际的类型参数。
Field field = Pair.class.getDeclaredField("myList"); // myList的类型是List
Type type = field.getGenericType();
if (type instanceof ParameterizedType) {
    ParameterizedType paramType = (ParameterizedType) type;
    Type[] actualTypes = paramType.getActualTypeArguments();
    for (Type aType : actualTypes) {
        if (aType instanceof Class) {
            Class clz = (Class) aType;
            System.out.println(clz.getName()); // 输出java.lang.String
        }    
    }
}

7.3动态代理
    熟悉设计模式的人对于代理模式可能都不陌生。  代理对象和被代理对象一般实现相同的接口,调用者与代理对象进行交互。代理的存在对于调用者来说是透明的,调用者看到的只是接口。代理对象则可以封装一些内部的处理逻辑,如访问控制、远程通信、日志、缓存等。比如一个对象访问代理就可以在普通的访问机制之上添加缓存的支持。
    
    JDK 5引入的动态代理机制,允许开发人员在运行时刻动态的创建出代理类及其对象。
    在运行时刻,可以动态创建出一个实现了多个接口的代理类。
    
    每个代理类的对象都会关联一个表示内部处理逻辑的InvocationHandler接口的实现。

    当使用者调用了代理对象所代理的接口中的方法的时候,这个调用的信息会被传递给InvocationHandler的invoke方法。

    在  invoke方法的参数中可以获取到代理对象、方法对应的Method对象和调用的实际参数。invoke方法的返回值被返回给使用者。这种做法实际上相当于对方法调用进行了拦截。

    下面的代码用来代理一个实现了List接口的对象。所实现的功能也非常简单,那就是禁止使用List接口中的add方法。
    public List getList(final List list) {
        return (List) Proxy.newProxyInstance(
                DummyProxy.class.getClassLoader(),
                new Class[] { List.class }, 
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method,Object[] args) 
                            throws Throwable {
                        if ("add".equals(method.getName())) {
                            throw new UnsupportedOperationException();
                        } else {
                            return method.invoke(list, args);
                        }
                    }
                });
    }


    Java 反射API实际上定义了一种相对于编译时刻而言更加松散的契约。如果被调用的Java对象中并不包含某个方法,而在调用者代码中进行引用的话,在编译时刻就会出现错误。而反射API则可以把这样的检查推迟到运行时刻来完成。通过把Java中的字节代码增强、类加载器和反射API结合起来,可以处理一些对灵活性要求很高的场景。

    在有些情况下,可能会需要从远端加载一个Java 类来执行。比如一个客户端Java程序可以通过网络从服务器端下载Java类来执行,从而可以实现自动更新的机制。当代码逻辑需要更新的时候,只需要部署一个新的Java类到服务器端即可。一般的做法是通过自定义类加载器下载了类字节代码之后,定义出  Class类的对象,再通过newInstance方法就可以创建出实例了。不过这种做法要求客户端和服务器端都具有某个接口的定义,从服务器端下载的是这个接口的实现。这样的话才能在客户端进行所需的类型转换,并通过接口来使用这个对象实例。如果希望客户端和服务器端采用更加松散的契约的话,使用反射API就可以了。两者之间的契约只需要在方法的名称和参数这个级别就足够了。服务器端Java类并不需要实现特定的接口,可以是一般的Java类。

    动态代理的使用场景就更加广泛了。需要使用AOP中的方法拦截功能的地方都可以用到动态代理。Spring框架的AOP实现默认也使用动态代理。不过JDK中的动态代理只支持对接口的代理,不能对一个普通的Java类提供代理。不过这种实现在大部分的时候已经够用了。

8.Java I / O 
    在应用程序中,通常会涉及到两种类型的计算:CPU计算和I/O 计算。对于大多数应用来说,花费在等待I/O 上的时间是占较大比重的。通常需要等待速度较慢的磁盘或是网络连接完成I/O 请求,才能继续后面的CPU计算任务。因此提高I/O 操作的效率对应用的性能有较大的帮助。

8.1流
    Java语言提供了多个层次不同的概念来对I/O操作进行抽象。Java I/O中最早的概念是流,包括输入流和输出流。
    
    流是一个连续的字节的序列。
    输入流是用来读取这个序列,而输出流则构建这个序列。
   
    InputStream和OutputStream所操纵的基本单元就是字节。每次读取和写入单个字节或是字节数组。
    
    读取或输出Java的基本数据类型
    (DataInputStream和DataOutputStream。它们所提供的类似readFloat和writeDouble这样的方法,会让处理基本数据类型变得很简单。

    读取或写入的是Java中的对象
    (ObjectInputStream和ObjectOutputStream。它们与对象的序列化机制一起,可以实现Java对象状态的持久化和数据传递。

    
8.2流的使用
    每个打开的流都需要被正确的关闭以释放资源。所遵循的原则是谁打开谁释放。
    如果一个流只在某个方法体内使用,则通过finally语句或是JDK 7 中的try-with-resources语句来确保在方法返回之前,流被正确的关闭

8.3缓冲区
    由于流背后的数据有可能比较大,在实际的操作中,通常会使用缓冲区来提高性能。传统的缓冲区的实现是使用数组来完成。
    (比如经典的从InputStream到OutputStream的复制的实现,就是使用一个字节数组作为中间的缓冲区。
     
    NIO中引入的Buffer及其子类,可以很方便的用来创建各种基本数据类型的缓冲区。

    在Buffer上进行的元素添加和删除操作,都围绕3个属性position、limit和capacity开,分别表示Buffer当前的读写位置、可用的读写范围和容量限制。
    (容量限制是在创建的时候指定的。Buffer提供的get/put方法都有相对和绝对两种形式。相对读写时的位置是相对于position的值,而绝对读写则需要指定起始的序号。
    (在使用Buffer的常见错误就是在读写操作时没有考虑到这3个元素的值,因为大多数时候都是使用的是相对读写操作,而position的值可能早就发生了变化。
    (将数据读入缓冲区之前,  需要调用clear方法;将缓冲区中的数据输出之前,需要调用flip方法。

    
8.4字符与编码
    在程序中,总是免不了与字符打交道,毕竟字符是用户直接可见的信息。而与字符处理直接相关的就是编码。
    (需要理解字符集和编码的概念

    字符集,顾名思义,就是字符的集合。一个字符集中所包含的字符通常与地区和语言有关。字符集中的每个字符通
常会有一个整数编码与其对应。常见的字符集有ASCII、ISO-8859-1和Unicode等。
    
    对于字符集中的每个字符,为了在计算机中表示,都需要转换某种字节的序列,即该字符的编码。同一个字符集可以有不同的编码方式。
    (如果某种编码格式产生的字节序列,用另外一种编码格式来解码的话,就可能会得到错误的字符,从而产生乱
码的情况。
    
    NIO中的java.nio.charset包提供了与字符集相关的类,可以用来进行编码和解码。其中的CharsetEncoderCharsetDecoder允许对编码和解码过程进行精细的控制,如处理非法的输入以及字符集中无法识别的字符等。
String input = "你123好";
Charset charset = Charset.forName("ISO-8859-1");
CharsetEncoder encoder = charset.newEncoder();
encoder.onUnmappableCharacter(CodingErrorAction.IGNORE);
CharsetDecoder decoder = charset.newDecoder();
CharBuffer buffer = CharBuffer.allocate(32);
buffer.put(input);
buffer.flip();
try {
ByteBuffer byteBuffer = encoder.encode(buffer);
CharBuffer cbuf = decoder.decode(byteBuffer);
System.out.println(cbuf); //输出123
} catch (CharacterCodingException e) {
e.printStackTrace();
}

    Java I/O在处理字节流字之外,还提供了处理字符流的类,即Reader/Writer类及其子类,它们所操纵的基本单位是char类型。在字节和字符之间的桥梁就是编码格式。通过编码器来完成这两者之间的转换。在创建  Reader/Writer子类实例的时候,总是应该使用两个参数的构造方法,即显式指定使用的字符集或编码解码器。如果不显式指定,使用的是JVM的默认字符集,有可能在其它平台上产生错误。

8.5通道
    通道作为NIO中的核心概念,在设计上比之前的流要好不少。通道相关的很多实现都是接口而不是抽象类。通道本身的抽象层次也更加合理。通道表示的是对支持I/O操作的实体的一个连接。一旦通道被打开之后,就可以执行读取和写入操作,而不需要像流那样由输入流或输出流来分别进行处理。与流相比,通道的操作使用的是Buffer而不是数组,使用更加方便灵活。通道的引入提升了I/O 操作的灵活性和性能,主要体现在文件操作和网络操作上。


8.6文件通道
    对文件操作方面,文件通道FileChannel提供了与其它通道之间高效传输数据的能力,比传统的基于流和字节数组作为缓冲区的做法,要来得简单和快速。比如下面的把一个网页的内容保存到本地文件的实现。
FileOutputStream output = new FileOutputStream("baidu.txt");
FileChannel channel = output.getChannel();
URL url = new URL("http://www.baidu.com");
InputStream input = url.openStream();
ReadableByteChannel readChannel = Channels.newChannel(input);
channel.transferFrom(readChannel, 0, Integer.MAX_VALUE);

    文件通道的另外一个功能是对文件的部分片段进行加锁。
    文件通道上的锁是由JVM所持有的,因此适合于与其它应用程序协同时使用。

    另外一个在性能方面有很大提升的功能是内存映射文件的支持。通过FileChannelmap方法可以创建出一个MappedByteBuffer对象,对这个缓冲区的操作都会直接反映到文件内容上
    这点尤其适合对大文件进行读写操作。
    
8.7套接字通道
    在套接字通道方面的改进是提供了对非阻塞I/O和多路复用I/O的支持。
    
    NIO中引入了非阻塞I/O的支持,不过只限于套接字I/O操作。所有继承SelectableChannel的通道类都可以通过configureBlocking方法来设置是否采用非阻塞模式。
    
    多路复用I/O是一种新的I/O编程模型。传统的套接字服务器的处理方式是对于每一个客户端套接字连接,都新创建一个线程来进行处理。创建线程是很耗时的操作,而有的实现会采用线程池。
    
    而多路复用  I/O的基本做法是由一个线程来管理多个套接字连接。该线程会负责根据连接的状态,来进行相应的处理。
    多路复用I/O依靠操作系统提供的select或相似系统调用的支持,选择那些已经就绪的套接字连接来处理。可以把多个非阻塞I/O通道注册在某Selector上,并声明所感兴趣的操作类型。每次调用Selector的select方法,就可以
选择到某些感兴趣的操作已经就绪的通道的集合,从而可以进行相应的处理。如果要执行的处理比较复杂,可以把处理转发给其它的线程来执行。
public class IOWorker implements Runnable{
    @Override
    public void run() {
        try {
            Selector selector = Selector.open();
            ServerSocketChannel channel = ServerSocketChannel.open();
            channel.configureBlocking(false);
            ServerSocket socket = channel.socket();
            socket.bind(new InetSocketAddress("localhost",10800));
            channel.register(selector, channel.validOps());
            while(true){
                selector.select();
                Iterator iterator = selector.selectedKeys().iterator();
                while(!iterator.hasNext()){
                    SelectionKey key = (SelectionKey) iterator.next();
                    iterator.remove();
                    if(!key.isValid()){
                        continue;
                    }
                    if(key.isAcceptable()){
                        ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                        SocketChannel sc = ssc.accept();
                        sc.configureBlocking(false);
                        sc.register(selector, sc.validOps());
                    }
                    if(key.isWritable()){
                        SocketChannel client = (SocketChannel) key.channel();
                        Charset charset = Charset.forName("UTF-8");
                        CharsetEncoder encoder = charset.newEncoder();
                        CharBuffer charBuffer = CharBuffer.allocate(32);
                        charBuffer.put("Hello World");
                        charBuffer.flip();
                        ByteBuffer content = encoder.encode(charBuffer);
                        client.write(content);
                        key.cancel();
                    }
                }
            }
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
目前来说最流行的两个Java NIO网络应用框架是Apache MINA和Netty。


9.Java安全
    安全性是Java应用程序的非功能性需求的重要组成部分,如同其它的非功能性需求一样,安全性很容易被开发人员所忽略。

9.1认证
    用户认证是应用安全性的重要组成部分,其目的是确保应用的使用者具有合法的身份。  

    Java安全中使用术语主体(Subject)来表示访问请求的来源。一个主体可以是任何的实体。一个主体可以有多个不同的身份标识(Principal)。比如一个应用的用户这类主体,就可以有用户名、身份证号码和手机号码等多种身份标识。除了身份标识之外,一个主体还可以有公开或是私有的安全相关的凭证(Credential),包括密码和密钥等。

    典型的用户认证过程是通过登录操作来完成的。在登录成功之后,一个主体中就具备了相应的身份标识。Java提供了一个可扩展的登录框架,使得应用开发人员可以很容易的定制和扩展与登录相关的逻辑。登录的过程由LoginContext启动。在创建LoginContext的时候需要指定一个登录配置(Configuration)的名称。该登录配置中包含了登录所需的多个LoginModule的信息。每个LoginModule实现了一种登录方式。当调用LoginContext的login方法的时候,所配置的每个LoginModule会被调用来执行登录操作。如果整个登录过程成功,则通过getSubject方法就可以获取到包含了身份标识信息的主体。开发人员可以实现自己的LoginModule来定制不同的登录逻辑。


9.2权限控制
    在验证了访问请求来源的合法身份之后,另一项工作是验证其是否具有相应的权限。权限由Permission及其子类来表示。
    每个权限都有一个名称,该名称的含义与权限类型相关。某些权限有与之对应的动作列表。

    比较典型的是文件操作权FilePermission,它的名称是文件的路径,而它的动作列表则包括读取、写入和执行等。Permission类中最重要的是implies方法,它定义了权限之间的包含关系,是进行验证的基础。

    权限控制包括管理和验证两个部分。
    管理指的是定义应用中的权限控制策略,而验证指的则是在运行时刻根据策略来判断某次请求是否合法。

    策略由Policy来表示,JDK提供了基于文件存储的基本实现。开发人员也可以提供自己的实现。
    (在应用运行过程中,只可能有一个Policy处于生效的状态。)

    验证部分的具体执行者是AccessController,其中的checkPermission方法用来验证给定的权限是否被允许。
    (在应用中执行相关的访问请求之前,都需要调用checkPermission方法来进行验证。AccessControlException

    JVM中内置提供了一些对访问关键部分内容的访问控制检查,不过只有在启动应用的时通过参数-Djava.security.manager启用了安全管理器之后才能生效,并与策略相配合。

    与访问控制相关的另外一个概念是特权动作。特权动作只关心动作本身所要求的权限是否具备,而并不关心调用者是谁。

    特权动作根据是否抛出受检异常,分为PrivilegedActionPrivilegedExceptionAction。这两个接口都只有一个run方法用来执行相关的动作,也可以向调用者返回结果。通过AccessController的doPrivileged方法就可以执行特权动作。

    Java安全使用了保护域的概念。每个保护域都包含一组类、身份标识和权限,其意义是在当访问请求的来源是这些身份标识的时候,这些类的实例就自动具有给定的这些权限。

    ProtectionDomain类用来表示保护域,它的两个构造方法分别用来支持静态和动态的权限。

9.3加密、解密与签名
    构建安全的Java应用离不开加密和解密。Java的密码框架采用了常见的服务提供者架构,以提供所需的可扩展性和互操作性。该密码框架提供了一系列常用的服务,包括加密、数字签名和报文摘要等。

    这些服务都有服务提供者接口(SPI),服务的实现者只需要实现这些接口,并注册到密码框架中即可。

    比如加密服务Cipher的SPI接口就是CipherSpi。每个服务都可以有不同的算法来实现。
    密码框架也提供了相应的工厂方法用来获取到服务的实例。
    (比如想使用采用MD5算法的报文摘要服务,只需要调用MessageDigest.getInstance("MD5")即可。)

    加密和解密过程中并不可少的就是密钥(Key)。

    加密算法一般分成对称和非对称种。

    对称加密算法使用同一个密钥进行加密和解密;
    而非对称加密算法使用一对公钥和私钥,一个加密的时候,另外一个就用来解密。
    
    不同的加密算法,有不同的密钥。对称加密算法使用的是SecretKey,而非对称加密算法则使用PublicKey
PrivateKey

    与密钥Key对应的另一个接口是KeySpec,用来描述不同算法的密钥的具体内容。
    比如一个典型的使用对称加密的方式如下:
KeyGenerator generator = KeyGenerator.getInstance("DES");
SecretKey key = generator.generateKey();
saveFile("key.data", key.getEncoded());
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.ENCRYPT_MODE, key);
String text = "Hello World";
byte[] encrypted = cipher.doFinal(text.getBytes());
saveFile("encrypted.bin", encrypted);
    加密的时候首先要生成一个密钥,再由Cipher服务来完成。可以把密钥的内容保存起来,方便传递给需要解密的程序。
byte[] keyData = getData("key.data");
SecretKeySpec keySpec = new SecretKeySpec(keyData, "DES");
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] data = getData("encrypted.bin");
byte[] result = cipher.doFinal(data);
    
    解密的时候先从保存的文件中得到密钥编码之后的内容,再通过SecretKeySpec获取到密钥本身的内容,再进行解密。

    报文摘要的目的在于防止信息被有意或无意的修改。通过对原始数据应用某些算法,可以得到一个校验码。当收到数据之后,只需要应用同样的算法,再比较校验码是否一致,就可以判断数据是否被修改过。相对原始数据来说,校验码长度更小,更容易进行比较。

    消息认证码(Message Authentication Code)与报文摘要类似,不同的是计算的过程中加入了密钥,只有掌握了密钥的接收者才能验证数据的完整性。

    使用公钥和私钥就可以实现数字签名的功能。某个发送者使用私钥对消息进行加密,接收者使用公钥进行解密。由于私钥只有发送者知道,当接收者使用公钥解密成功之后,就可以判定消息的来源肯定是特定的发送者。这就相当于发送者对消息进行了签名。

    数字签名由Signature服务提供,签名和验证的过程都比较直接。

9.4安全套接字连接
    安全套接字连接指的是对套接字连接进行加密。

    非对称加密算法则适合于这种情况。私钥自己保管,公钥则公开出去。发送数据的时候,用私钥加密,接收者用公开的公钥解密;接收数据的时候,则正好相反。这种做法解决了共享密钥的问题,但是另外的一个问题是如何确保接收者所得到的公钥确实来自所声明的发送者,而不是伪造的。为此,又引入了证书的概念。
    
    证书中包含了身份标识和对应的公钥。证书由用户所信任的机构签发,并用该机构的私钥来加密。在有些情况下,某个证书签发机构的真实性会需要由另外一个机构的证书来证明。通过这种证明关系,会形成一个证书的链条。而链条的根则是公认的值得信任的机构。只有当证书链条上的所有证书都被信任的时候,才能信任证书中所给出的公钥。 

    日常开发中比较常接触的就是HTTPS,即安全的HTTP连接。大部分用Java程序访问采用HTTPS网站时出现的错误都与证书链条相关。有些网站采用的不是由正规安全机构签发的证书,或是证书已经过期。如果必须访问这样的HTTPS网站的话,可以提供自己的套接字工厂和主机名验证类来绕过去。

    另外一种做法是通过keytool工具把证书导入到系统的信任证书库之中。
    
URL url = new URL("https://localhost:8443");
SSLContext context = SSLContext.getInstance("TLS");
context.init(new KeyManager[] {}, 
    new TrustManager[] {new MyTrustManager()}, 
    new SecureRandom());
HttpsURLConnection connection =(HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(context.getSocketFactory());
connection.setHostnameVerifier(new MyHostnameVerifier());


10.Java对象序列化与RMI
    对于一个存在于Java虚拟机中的对象来说,其内部的状态只保持在内存中。JVM停止之后,这些状态就丢失了。在很多情况下,对象的内部状态是需要被持久化下来的。提到持久化,最直接的做法是保存到文件系统或是数据库之中。这种做法一般涉及到自定义存储格式以及繁琐的数据转换。

    对象关系映射(Object-relational mapping)是一种典型的用关系数据库来持久化对象的方式,也存在很多直接存储对象的对象数据库。
    
    对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。除了可以很简单的实现持久化之外,序列化机制的另外一个重要用途是在远程方法调用中,用来对开发人员屏蔽底层实现细节。

10.1基本对象序列化
    待序列化的Java类只需要实现Serializable接口即可。

    实际的序列化和反序列化工作是通过ObjectOuputStreamObjectInputStream来完成的。

    ObjectOutputStream的writeObject方法可以把一个Java对象写入到流中,
    ObjectInputStream的readObject方  法可以从流中读取一个Java对象。

    (在写入和读取的时候,虽然用的参数或返回值是单个对象,但实际上操纵的是一个对象图,包括该对象所引用的其它对象,以  及这些对象所引用的另外的对象。Java会自动帮你遍历对象图并逐个序列化。

    除了对象之外,Java中的基本类型和数组也是可以通过  ObjectOutputStream和ObjectInputStream来序列化的。

try {
User user = new User("Alex", "Cheng");
ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("user.bin"));
output.writeObject(user);
output.close();
} catch (IOException e) {
 e.printStackTrace();
}
try {
ObjectInputStream input = new ObjectInputStream(new FileInputStream("user.bin"));
User user = (User) input.readObject();
System.out.println(user);
} catch (Exception e) {
e.printStackTrace();
}

上面的代码给出了典型的把Java对象序列化之后保存到磁盘上,以及从磁盘上读取的基本方式。User类只是声明了实现Serializable接口。

    在默认的序列化实现中,Java对象中的非静态和非瞬时域都会被包括进来,而与域的可见性声明没有关系。这可能会导致某些不应该出现的域被包含在序列化之后的字节数组中,比如密码等隐私信息

    由于Java对象序列化之后的格式是固定的,其它人可以很容易的从中分析出其中的各种信息。

    对于这种情况,一种解决办法是把域声明为瞬时的,即使用 transient 关键词。

    另外一种做法是添加一个serialPersistentFields? 域来声明序列化时要包含的域。
private static final ObjectStreamField[] serialPersistentFields = {
    new ObjectStreamField("firstName", String.class)
};

10.2自定义对象序列化
    基本的对象序列化机制让开发人员可以在包含哪些域上进行定制。如果想对序列化的过程进行更加细粒度的控制,就需要在类中添加writeObject和对应的  readObject方法。

10.3序列化时的对象替换
    在有些情况下,可能会希望在序列化的时候使用另外一个对象来代替当前对象。
    替换对象的作用类似于Java EE中会使用到的传输对象(Transfer Object)。

    readResolve writeReplace

    在Order类的writeReplace方法中返回了一个OrderReplace对象。这个对象会被作为替代写入到流中。同样的,
需要在OrderReplace类中定义一个readResolve 方法,用来在读取的时候再转换回Order类对象。这样对调用者来说,替换对象的存在就是透明的。

10.4序列化对象的创建
    在通过ObjectInputStream的readObject方法读取到一个对象之后,这个对象是一个新的实例,但是其构造方法是没有被调用的,其中的域的初始化代码也没有被执行。对于那些没有被序列化的域,在新创建出来的对象中的值都是默认的。也就是说,这个对象从某种角度上来说是不完备的。这有可能会造成一些隐含的错误。调用者并不知道对象是通过一般的new操作符来创建的,还是通过反序列化所得到的。解决的办法就是在类的readObject方法里面,再执行所需的对象初始化逻辑。对于一般的Java类来说,构造方法中包含了初始化的逻辑。可以把这些逻辑提取到一个方
法中,在readObject方法中调用此方法

10.5序列化安全性
    对序列化之后的流进行加密。这可以通过CipherOutputStream来实现。
    实现自己的writeObject和readObject 方法,在调用defaultWriteObject之前,先对要序列化的域的值进行加密处理。

    使用一个SignedObject或SealedObject来封装当前对象,用SignedObject或SealedObject进行序列化。
在从流中进行反序列化的时候,可以通过ObjectInputStream的registerValidation方法添加ObjectInputValidation接口的实现,用来验证反序列化之后得到的对象是否合法。

    在从流中进行反序列化的时候,可以通过ObjectInputStream的registerValidation方法添加ObjectInputValidation接口的实现,用来验证反序列化之后得到的对象是否合法。

10.6 RMI
    RMI(Remote Method Invocation)是Java中的远程过程调用(Remote Procedure Call,RPC)实现,是一种分布式Java应用的实现方式。它的目的在于对开发人员屏蔽横跨不同JVM和网络连接等细节,使得分布在不同JVM上的对  象像是存在于一个统一的JVM中一样,可以很方便的互相通讯。

    开发人员可以基于Apache MINA或是Netty这样的框架来写自己的网络服务器,亦或是可以采用REST架构风格来  编写HTTP服务。
    (但这些解决方案中,不可回避的一个部分就是数据的编排和解排(marshal/unmarshal)

    RMI采用的是典型的客户端-服务器端架构。
    
    首先需要定义的是服务器端的远程接口,这一步是设计好服务器端需要提供什么样的服务。
    (对远程接口的要求很简单,只需要继承自RMI中的Remote接口即可。
        程接口中的方法需要抛出RemoteException。

    实现了远程接口的类的实例称为远程对象。创建出远程对象之后,需要把它注册到一个注册表之中。这是为了客户端能够找到该远程对象并调用。

    为了通过Java的序列化机制来进行传输,远程接口中的方法的参数和返回值,要么是Java的基本类型,要么是远程对象,要么是实现了  Serializable接口的Java类。

    当客户端通过RMI注册表找到一个远程接口的时候,所得到的其实是远程接口的一个动态代理对象。当客户端调用  其中的方法的时候,方法的参数对象会在序列化之后,传输到服务器端。服务器端接收到之后,进行反序列化得到参数对象。并使用这些参数对象,在服务器端调用  实际的方法。调用的返回值Java对象经过序列化之后,再发送回客户端。客户端再经过反序列化之后得到Java对象,返回给调用者。这中间的序列化过程对于使用者来说是透明的,由动态代理对象自动完成。除了序列化之外,RMI还使用了动态类加载技术。当需要进行反序列化的时候,如果该对象的
类定义在当前JVM中没有找到,RMI会尝试从远端下载所需的类文件定义。可以在RMI程序启动的时候,通过JVM参数java.rmi.server.codebase来指定动态下载Java类文件的URL。




Java explore,布布扣,bubuko.com

Java explore

原文:http://www.cnblogs.com/xiaosheng-aquarius/p/3586078.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!