diff --git a/OOP.md b/OOP.md new file mode 100644 index 0000000..f90cf6a --- /dev/null +++ b/OOP.md @@ -0,0 +1,353 @@ +# 面向对象编程 + +# JAVA 面向对象基础 + +# 面向对象 +## 封装的原则 + +要求使对象之外的部分不能随意存取对象的内部数据,从而有效避免了错误对它的“交叉感染”,使软件错误能局部化,降低排错难度 + +## 继承 + +所有的类都继承自 java.lang.Object,一些常用的方法: + +equals():比较两个对象引用时否相同。 + +getClass():返回对象运行时所对应的类的表示,从而得到相应的信息 + +toString():返回对象字符串表示 + +finalize():用于在垃圾收集前清除对象 + +notify(), notifyall(), wait(): 用于多线程处理中的同步 + +子类(subclass)对父类(superclass,超类)的继承 + +子类不能继承父类中访问权限为 private 的成员变量和方法。 + +子类可以重写父类的方法,及命名与父类同名的成员变量。 + +Java 不支持多重继承 + +创建子类 + +``` +    class SubClass extends SuperClass { + +      ... + +    } +``` + +成员的隐藏和方法的重写 + +子类通过隐藏父类的成员变量和重写父类的方法,可以把父类的状态和行为变为自身的状态和行为。 + +## 多态性 +子类继承父类后,同一个方法有不同的表现 + +体现在两个方面:方法重载实现的静态多态性(编译时多态),方法重写实现的动态多态性(运行时多态) + +重写方法的调用原则:子类重写父类的方法,调用子类方法;反之,调用父类的方法 + +一个对象可以引用子类的实例来调用子类的方法 + +eg: B 继承 A,A 的对象 a 引用 B 的实例,调用 B 的方法 callme() + +``` +import java.io.*; +class A { + void callme() { + System.out.println("Inside A's callme()) method"); + } +} + +class B extends A { + void callme() { + System.out.println("Inside B's callme() method"); + } +} + +public class Dispatch { + public static void main(String args[]) { + A a = new B(); // 引用子类的实例 + a.callme(); + } +} +``` + +![](images/25.png) + +## 类的实现 +类声明 +``` +    [public][abstract|final] class className [extends superclassName] [implements interfaceNameList] {} +      修饰符public, abstract, final说明类的属性 +      className为类的属性 +      superclassName为父类的名字 +      interfaceNameList为类所实现的接口列表 +``` + +类体 + +``` +    class className +    { +      [public | protected | private] [static] [final] [transient] [volatile] type variableName; // 成员变量 +      [public | protected | private] [static] [final | abstract] [native] [synchronized] returnType methodName( +        [paramList]) [throws exceptionList] {statements}; //成员方法 +    } +``` + +成员变量 + +``` +    [public | protected | private] [static] [final] [transient] [volatile] type variableName; // 成员变量 +      static: 静态变量(类变量) +      final: 常量 +      transient:暂时性变量,用于对象存档 +      volatile:共享变量,用于并发线程的共享 +``` + +成员方法 + +``` +    [public | protected | private] [static] [final | abstract] [native] [synchronized] returnType methodName( +    [paramList]) [throws exceptionList] {statements}; //成员方法 +      static: 类方法,可通过类名直接调用 +      abstract: 抽象方法,没有方法体 +      final:方法不能被重写 +      native:集成其他语言的代码 +      synchronized:控制多个并发线程的访问 +``` + +Java 类中的限定词: + +private:类中限定为 private 的成员,只能被这个类本身访问。如果构造方法为 private,则其他类不能实例化该类。 + +default:不加任何访问权限,可以被这个类本身和同一个包中的类访问。 + +protected:类中限定为 protected 的成员,可以被这个类本身、它的子类和同一个包中的其他类访问。 + +public:类中被限定为 public 的成员,可以被所有类访问。 + +final 关键字可以修饰类、类的成员变量和成员方法,但作用不同 + +修饰成员变量:称为常量,须给出初始值 + +修饰成员方法:该方法不能被子类重写 + +修饰类:类不能被继承 + +super: 访问父类的成员  + +访问父类被隐藏的成员变量,如 super.variable; + +调用父类中被重写的方法,如 super.Method([paramlist]); + +调用父类的构造函数,如 super([paramlist]); + +eg: + +``` +import java.io.*; +class SuperClass { + int x; + + SuperClass() { + x = 3; + System.out.println("in SuperClass: x = " + x); + } + + void doSomething() { + System.out.println("in SuperClass.doSomething()"); + } +} + +class SubClass extends SuperClass { + int x; + + SubClass() { + super(); + x = 5; + System.out.println("in SubClass: x = " + x); + } + + void doSomething() { + super.doSomething(); + System.out.println("in SubClass.doSomething()"); + System.out.println("Super.x = " + super.x + "sub.x = " + x); + } +} + +public class Inhereritance { + + public static void main(String chars[]) { + SubClass sc = new SubClass(); + sc.doSomething(); + } +} +``` + +![](images/26.png) + +## 简单数据:值类型 +复合数据:引用类型 + +``` +import java.io.*; +public class PassTest { + float ptValue; + + public static void main(String args[]) { + int val; + PassTest pt = new PassTest(); + val = 11; + System.out.println("Original int Value is:"+val); + pt.changeInt(val); + System.out.println("Int Value after Change is:"+val); + pt.ptValue = 101f; + System.out.println("Original ptValue is:"+pt.ptValue); + pt.changeObjectValue(pt); // 引用类型的参数 + System.out.println("ptValue after change is:"+pt.ptValue); + + } + + public void changeInt(int value) { + value = 55; + } + + public void changeObjectValue(PassTest ref) { + ref.ptValue = 99f; + } +} +``` + +![](images/27.png) + +简单数据类型作为参数传递时,为值传递;复合数据类型作为参数传递时,为地址传递 + +方法体 +方法的实现。方法体中局部变量若与成员变量同名,局部变量将屏蔽成员变量。 + +``` +import java.io.*; +class Variable { + int x = 0, y = 0, z = 0; // 类的成员变量 + + void init(int x, int y) { + this.x = x; + this.y = y; + int z = 5; // 局部变量 + System.out.println("** in init**"); + System.out.println("x = " + x + "y = " + y + "z = " + z); + } +} + +public class VariableTest { + public static void main(String args[]) { + Variable v = new Variable(); + System.out.println("** before init **"); + System.out.println("x = " + v.x + "y = " + v.y + "z = " + v.z); + v.init(20, 30); + System.out.println("** after init **"); + System.out.println("x = " + v.x + "y = " + v.y + "z = " + v.z); + } +} +``` + +![](images/28.png) + +方法重载 + +指多个方法享有相同的名字。这些方法的参数必须不同。且参数类型区分度要足够:如不能使同一简单类型的数据:int 与 long + +``` +import java.io.*; + +class MethodOverloading { + void receive(int i) { + System.out.println("Receive in data"); + System.out.println("i = " + i); + } + + void receive(int x, int y) { + System.out.println("Receive two in datas"); + System.out.println("x = " + x + "y = " + y); + } +} + +public class MethodOverloadingTest { + public static void main(String args[]) { + MethodOverloading mo = new MethodOverloading(); + mo.receive(1); + mo.receive(2, 3); + } +} +``` + +![](images/29.png) + +## 构造方法 + +一个特殊的方法。每个类都有构造方法,用来初始化该类的一个对象。 + +构造方法具有和类名相同的名称,不返回任何数据类型。 + +重载经常用于构造方法。 + +构造方法只能由 new 运算符调用 + +### 抽象类和抽象方法: + +用 abstract 关键字修饰类:抽象类 + +用 abstract 关键字修饰方法:抽象方法 + +抽象类必须被继承,抽象方法必须被重写 + +抽象方法只需声明,无需实现 + +抽象类不能被实例化,抽象类不一定要包含抽象方法 + +若类中包含抽象方法,给类必须被定义为抽象类 + +## 接口 + +接口是抽象类的一种,只包含常量和方法的定义,没有变量和方法的实现,且其方法都是抽象方法。 + +用处体现在: + +通过接口,实现不相关类的相同行为 + +通过接口,指明多个类需要实现的方法 + +通过接口,了解对象的交互界面,无需了解对象所对应的类 + +### 接口的定义: + +接口声明: + +``` +[public] interface interfaceName[extends listOfSuperInterface] {...} +``` + +方法体定义: + +``` +returnType methodName([paramlist]); +``` + +接口的实现: + +在类的声明中用 implements 子句来表示一个类使用某个接口 + +类体中可以使用接口中定义的常量,必须实现接口中定义的所有方法 + +一个类可以实现多个接口,在 implements 中用逗号隔开 + +接口类型的使用: + +接口作为一种引用类型来使用 + +任何实现该接口的类的实例,都可以存储在该接口类型的变量中,通过这些实例,访问该类接口中的方法。 \ No newline at end of file diff --git a/TOC.md b/TOC.md index 096f831..3ce54e2 100644 --- a/TOC.md +++ b/TOC.md @@ -1,54 +1,32 @@ - J2SE 基础 - - [搭建 Java 开发环境(小明)](environment.md) - - [Java 基本概念](basic-concept.md) - - [面向对象编程](OOP.md) - - [关键字](keyword.md) - - [基本类型与运算](type-operation.md) + - [Java 开发环境](environment.md) + - [基本类型与运算符](type-operation.md) - [字符串与数组](string-array.md) + - [面向对象编程](OOP.md) - [输入输出流](io.md) - [集合类](collection.md) - [Java 虚拟机](virtual-machine.md) - - [Java 平台与内存管理](platorm-memory.md) + - [Java 内存管理](platorm-memory.md) - [Java 泛型](generic.md) - [异常处理](exception.md) - - [XML](xml.md) - [Java 多线程](multi-thread.md) - - [Java 版本特性](version-difference.md) - - [Java 相关工具](tools.md) - - [JUnit](junit.md) - Java Web 开发 - - [Web 基础知识](web-basic.md) - [Servlet 与 JSP](servlet-jsp.md) - - [J2EE 与 EJB](j2ee-ejb.md) - [Webservice](webservice.md) - - [Tomcat](tomcat.md) - - [Weblogic](weblogic.md) - Java 常用框架 - - [Struts](struts.md) + - [Struts2](struts.md) - [Spring](spring.md) - [Hibernate](hibernate.md) - [Netty](netty.md) -- Java 软件工程与设计模式 - - [UML](uml.md) - - [常见设计模式](design-pattern.md) - - [软件工程](software-engineering.md) -- Java 操作系统、数据库和网络 - - [操作系统](os.md) - - [数据库及 SQL 语句](db-sql.md) +- Java 数据库和网络 + - [Java 数据库操作](db-sql.md) - [JDBC](jdbc.md) - - [Oracle](oracle.md) - - [MySQL](mysql.md) - - [网络](network.md) - [Socket](socket.md) -- Java 新技术 +- Java 其他、补充 - [海量数据处理](massive.md) - - [NoSQL](nosql.md) - - [云计算](cloud-computing.md) - - [Hadoop](hadoop.md) -- Java 数据结构和算法 + - [常见设计模式](design-pattern.md) - [排序算法](sort.md) - - [Java 常用算法](common-algorithm.md) - - [Java 面试常用算法](interview.md) + diff --git a/collection.md b/collection.md new file mode 100644 index 0000000..72b2ed8 --- /dev/null +++ b/collection.md @@ -0,0 +1,191 @@ +# 集合类 + +# Java 集合类详解 + +# 集合类说明及区别 + +Collection +├List +│├LinkedList +│├ArrayList +│└Vector +│ └Stack +└Set +Map +├Hashtable +├HashMap +└WeakHashMap + +## Collection 接口 + +Collection 是最基本的集合接口,一个 Collection 代表一组 Object,即 Collection 的元素(Elements)。一些 Collection 允许相同的元素而另一些不行。一些能排序而另一些不行。Java SDK 不提供直接继承自 Collection 的类,Java SDK 提供的类都是继承自 Collection的“子接口”如 List 和 Set。 +   +所有实现 Collection 接口的类都必须提供两个标准的构造函数:无参数的构造函数用于创建一个空的 Collection,有一个 Collection 参数的构造函数用于创建一个新的 Collection,这个新的 Collection 与传入的 Collection 有相同的元素。后 一个构造函数允许用户复制一个Collection。 +   +如何遍历 Collection 中的每一个元素?不论 Collection 的实际类型如何,它都支持一个iterator()的方法,该方法返回一个迭代子,使用该迭代子即可逐一访问 Collection 中每一个元素。典型的用法如下: + +``` +    Iterator it = collection.iterator(); // 获得一个迭代子 +    while(it.hasNext()) { +      Object obj = it.next(); // 得到下一个元素 +    } +``` + +由 Collection 接口派生的两个接口是 List 和 Set。 + +### List 接口 +   +List 是有序的 Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在 List 中的位置,类似于数组下标)来访问 List 中的元素,这类似于 Java 的数组。 + +和下面要提到的 Set 不同,List 允许有相同的元素。 +   +除了具有 Collection 接口必备的 iterator()方法外,List 还提供一个 listIterator()方法,返回一个 ListIterator 接口,和标准的 Iterator 接口相比,ListIterator 多了一些 add()之类的方法,允许添加,删除,设定元素, 还能向前或向后遍历。 +   +实现 List 接口的常用类有 LinkedList,ArrayList,Vector 和 Stack。 + +### LinkedList 类 +   +LinkedList 实现了 List 接口,允许 null 元素。此外 LinkedList 提供额外的 get,remove,insert 方法在 LinkedList 的首部或尾部。这些操作使 LinkedList 可被用作堆栈(stack),队列(queue)或双向队列(deque)。 +   +注意 LinkedList 没有同步方法。如果多个线程同时访问一个 List,则必须自己实现访问同步。一种解决方法是在创建 List 时构造一个同步的 List: +     +``` +List list = Collections.synchronizedList(new LinkedList(...)); +``` + +### ArrayList 类 +   +ArrayList 实现了可变大小的数组。它允许所有元素,包括 null。ArrayList 没有同步。 +size,isEmpty,get,set 方法运行时间为常数。但是 add 方法开销为分摊的常数,添加 n 个元素需要 O(n)的时间。其他的方法运行时间为线性。 +   +每个 ArrayList 实例都有一个容量(Capacity),即用于存储元素的数组的大小。这个容量可随着不断添加新元素而自动增加,但是增长算法 并没有定义。当需要插入大量元素时,在插入前可以调用 ensureCapacity 方法来增加 ArrayList 的容量以提高插入效率。 +   +和 LinkedList 一样,ArrayList 也是非同步的(unsynchronized)。 + +### Vector 类 +   +Vector 非常类似 ArrayList,但是 Vector 是同步的。由 Vector 创建的 Iterator,虽然和 ArrayList 创建的 Iterator 是同一接口,但是,因为 Vector 是同步的,当一个 Iterator 被创建而且正在被使用,另一个线程改变了 Vector 的状态(例如,添加或删除了一些元素),这时调用 Iterator 的方法时将抛出 ConcurrentModificationException,因此必须捕获该异常。 + +### Stack 类 +   +Stack 继承自 Vector,实现一个后进先出的堆栈。Stack 提供5个额外的方法使得 Vector 得以被当作堆栈使用。基本的 push 和 pop 方法,还有 peek 方法得到栈顶的元素,empty 方法测试堆栈是否为空,search 方法检测一个元素在堆栈中的位置。Stack 刚创建后是空栈。 + +### Set 接口 +   +Set 是一种不包含重复的元素的 Collection,即任意的两个元素 e1 和 e2 都有 e1.equals(e2)=false,Set 最多有一个 null 元素。 +   +很明显,Set 的构造函数有一个约束条件,传入的 Collection 参数不能包含重复的元素。 +   +请注意:必须小心操作可变对象(Mutable Object)。如果一个 Set 中的可变元素改变了自身状态导致 Object.equals(Object)=true 将导致一些问题。 + +### Map 接口 +   +请注意,Map 没有继承 Collection 接口,Map 提供 key 到 value 的映射。一个 Map 中不能包含相同的 key,每个 key 只能映射一个 value。Map 接口提供3种集合的视图,Map 的内容可以被当作一组 key 集合,一组 value 集合,或者一组 key-value 映射。 + +### Hashtable 类 +   +Hashtable 继承 Map 接口,实现一个 key-value 映射的哈希表。任何非空(non-null)的对象都可作为 key 或者 value。 +   +添加数据使用 put(key, value),取出数据使用 get(key),这两个基本操作的时间开销为常数。 + +Hashtable 通过 initial capacity 和 load factor 两个参数调整性能。通常缺省的 load factor 0.75较好地实现了时间和空间的均衡。增大 load factor 可以节省空间但相应的查找时间将增大,这会影响像 get 和 put 这样的操作。 + +使用 Hashtable 的简单示例如下,将1,2,3放到 Hashtable 中,他们的 key 分别是”one”,”two”,”three”: + +``` +    Hashtable numbers = new Hashtable(); +    numbers.put(“one”, new Integer(1)); +    numbers.put(“two”, new Integer(2)); +    numbers.put(“three”, new Integer(3)); +``` + +要取出一个数,比如2,用相应的 key: + +``` +    Integer n = (Integer)numbers.get(“two”); +    System.out.println(“two = ” + n); +``` + +由于作为 key 的对象将通过计算其散列函数来确定与之对应的 value 的位置,因此任何作为 key的对象都必须实现 hashCode 和 equals 方法。hashCode 和 equals 方法继承自根类 Object,如果你用自定义的类当作 key 的话,要相当小心,按照散列函数的定义,如果两个对象相同,即 obj1.equals(obj2)=true,则它们的 hashCode 必须相同,但如果两个对象不同,则它们的 hashCode 不一定不同,如果两个不同对象的 hashCode 相同,这种现象称为冲突,冲突会导致操作哈希表的时间开销增大,所以尽量定义好的 hashCode()方法,能加快哈希表的操作。 +   +如果相同的对象有不同的 hashCode,对哈希表的操作会出现意想不到的结果(期待的 get 方法返回 null),要避免这种问题,只需要牢记一条:要同时复写 equals 方法和 hashCode 方法,而不要只写其中一个。 + +Hashtable 是同步的。 + +### HashMap 类 +   +HashMap 和 Hashtable 类似,不同之处在于 HashMap 是非同步的,并且允许 null,即 null value 和 null key。,但是将 HashMap 视为 Collection 时(values()方法可返回Collection),其迭代子操作时间开销和 HashMap 的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将 HashMap 的初始化容量设得过高,或者 load factor 过低。 + +### WeakHashMap 类 +   +WeakHashMap 是一种改进的 HashMap,它对 key 实行“弱引用”,如果一个 key 不再被外部所引用,那么该 key 可以被 GC 回收。 + +# 总结 +   +如果涉及到堆栈,队列等操作,应该考虑用 List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,应该使用 ArrayList。 +   +如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高,如果多个线程可能同时操作一个类,应该使用同步的类。 +   +要特别注意对哈希表的操作,作为 key 的对象要正确复写 equals 和 hashCode 方法。 +   +尽量返回接口而非实际的类型,如返回 List 而非 ArrayList,这样如果以后需要将 ArrayList换成 LinkedList 时,客户端代码不用改变。这就是针对抽象编程。 + +# 同步性 + +Vector 是同步的。这个类中的一些方法保证了 Vector 中的对象是线程安全的。而 ArrayList 则是异步的,因此 ArrayList 中的对象并不是线程安全的。因为同步的要求会影响执行的效率,所以如果你不需要线程安全的集合那么使用 ArrayList 是一个很好的选择,这样可以避免由于同步带来的不必要的性能开销。 + +数据增长 + +从内部实现机制来讲 ArrayList 和 Vector 都是使用数组(Array)来控制集合中的对象。当你向这两种类型中增加元素的时候,如果元素的数目 超出了内部数组目前的长度它们都需要扩展内部数组的长度,Vector 缺省情况下自动增长原来一倍的数组长度,ArrayList 是原来的50%,所以最 后你获得的这个集合所占的空间总是比你实际需要的要大。所以如果你要在集合中保存大量的数据那么使用 Vector 有一些优势,因为你可以通过设置集合的初 始化大小来避免不必要的资源开销。 + +使用模式 + +在 ArrayList 和 Vector 中,从一个指定的位置(通过索引)查找数据或是在集合的末尾增加、移除一个元素所花费的时间是一样的,这个时间我们用 O(1)表示。但是,如果在集合的其他位置增加或移除元素那么花费的时间会呈线形增长:O(n-i),其中 n 代表集合中元素的个数,i 代表元素增加或移除元素的索引位置。为什么会这样呢?以为在进行上述操作的时候集合中第 i 和第 i 个元素之后的所有元素都要执行位移的操作。这一切意味着什么呢? + +这意味着,你只是查找特定位置的元素或只在集合的末端增加、移除元素,那么使用 Vector 或ArrayList 都可以。如果是其他操作,你最好选择其他的集合操作类。比如,LinkList 集合类在增加或移除集合中任何位置的元素所花费的时间都是一样的?O(1),但它在索引一个元素的使用缺比较慢 -O(i),其中 i 是索引的位置.使用 ArrayList 也很容易,因为你可以简单的使用索引来代替创建 iterator 对象的操作。LinkList 也会为每个插入的元素创建对象,所有你要明白它也会带来额外的开销。 + +最后,在《Practical Java》一书中 Peter Haggar 建议使用一个简单的数组(Array)来代替 Vector 或 ArrayList。尤其是对于执行效率要求高的程序更应如此。因为使用数组 (Array)避免了同步、额外的方法调用和不必要的重新分配空间的操作。 + +# 相互区别 + +## Vector 和 ArrayList + +1,vector 是线程同步的,所以它也是线程安全的,而 arraylist 是线程异步的,是不安全的。如果不考虑到线程的安全因素,一般用 arraylist 效率比较高。 + +2,如果集合中的元素的数目大于目前集合数组的长度时,vector 增长率为目前数组长度的100%,而 arraylist 增长率为目前数组长度的50%.如过在集合中使用数据量比较大的数据,用 vector 有一定的优势。 + +3,如果查找一个指定位置的数据,vector 和 arraylist 使用的时间是相同的,都是0(1),这个时候使用 vector 和 arraylist 都可以。而如果移动一个指定位置的数据花费的时间为 0(n-i)n 为总长度,这个时候就应该考虑到使用 linklist,因为它移动一个指定位置的数据所花费的时间为 0(1),而查询一个指定位置的数据时花费的时间为 0(i)。 + +ArrayList 和 Vector 是采用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,都允许直接序号索引元素,但是插入数据要设计到数组元素移动 等内存操作,所以索引数据快插入数据慢,Vector 由于使用了 synchronized 方法(线程安全)所以性能上比ArrayList 要差,LinkedList 使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,但是插入数据时只需要记录本项的前后项即可,所以插入数度较快! + +## arraylist 和 linkedlist + +1.ArrayList 是实现了基于动态数组的数据结构,LinkedList 基于链表的数据结构。 +2.对于随机访问 get 和 set,ArrayList 觉得优于 LinkedList,因为 LinkedList 要移动指针。 +3.对于新增和删除操作 add 和 remove,LinedList 比较占优势,因为 ArrayList 要移动数据。 +这一点要看实际情况的。若只对单条数据插入或删除,ArrayList 的速度反而优于 LinkedList。但若是批量随机的插入删除数据,LinkedList 的速度大大优于 ArrayList. 因为 ArrayList 每插入一条数据,要移动插入点及之后的所有数据。 + +## HashMap 与 TreeMap +(注) +文章出处:http://www.diybl.com/course/3_program/java/javaxl/200875/130233.html + +1、HashMap 通过 hashcode 对其内容进行快速查找,而 TreeMap 中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用 TreeMap(HashMap 中元素的排列顺序是不固定的)。 + +HashMap 中元素的排列顺序是不固定的)。 + +2、HashMap 通过 hashcode 对其内容进行快速查找,而 TreeMap 中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用 TreeMap(HashMap 中元素的排列顺序是不固定的)。集合框架”提供两种常规的 Map 实现:HashMap 和 TreeMap (TreeMap 实现SortedMap 接口)。 + +3、在 Map 中插入、删除和定位元素,HashMap 是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么 TreeMap 会更好。使用 HashMap 要求添加的键类明确定义了 hashCode()和 equals()的实现。  这个 TreeMap 没有调优选项,因为该树总处于平衡状态。 + +结过研究,在原作者的基础上我还发现了一点,二树 map 一样,但顺序不一样,导致 hashCode()不一样。 +同样做测试: +在 hashMap 中,同样的值的 map,顺序不同,equals 时,false; +而在 treeMap 中,同样的值的 map,顺序不同,equals 时,true,说明,treeMap 在 equals()时是整理了顺序了的。 + +## hashtable 与 hashmap + +一.历史原因:Hashtable 是基于陈旧的 Dictionary 类的,HashMap 是 Java 1.2 引进的 Map 接口的一个实现 + +二.同步性:Hashtable 是线程安全的,也就是说是同步的,而 HashMap 是线程序不安全的,不是同步的 + +三.值:只有 HashMap 可以让你将空值作为一个表的条目的 key 或 value \ No newline at end of file diff --git a/db-sql.md b/db-sql.md new file mode 100644 index 0000000..cbe319a --- /dev/null +++ b/db-sql.md @@ -0,0 +1,858 @@ +# Java 数据库操作 + +# java 程序员从笨鸟到菜鸟之(七)—java 数据库操作 + +数据库访问几乎每一个稍微成型的程序都要用到的知识,怎么高效的访问数据库也是我们学习的一个重点,今天的任务就是总结 java 访问数据库的方法和有关 API,java 访问数据库主要用的方法是 JDBC,它是 java 语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法,下面我们就具体来总结一下 JDBC +一:Java 访问数据库的具体步骤: +1. 加载(注册)数据库 +驱动加载就是把各个数据库提供的访问数据库的 API 加载到我们程序进来,加载 JDBC 驱动,并将其注册到 DriverManager 中,每一种数据库提供的数据库驱动不一样,加载驱动时要把 jar 包添加到 lib 文件夹下,下面看一下一些主流数据库的 JDBC 驱动加裁注册的代码: + +``` +//Oracle8/8i/9iO数据库(thin模式) +Class.forName("oracle.jdbc.driver.OracleDriver").newInstance(); +//Sql Server7.0/2000数据库 Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver"); +//Sql Server2005/2008数据库 Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); +//DB2数据库 +Class.froName("com.ibm.db2.jdbc.app.DB2Driver").newInstance(); +//MySQL数据库 Class.forName("com.mysql.jdbc.Driver").newInstance(); +``` + +2. 建立链接    +建立数据库之间的连接是访问数据库的必要条件,就像南水北调调水一样,要想调水首先由把沟通的河流打通。建立连接对于不同数据库也是不一样的,下面看一下一些主流数据库建立数据库连接,取得 Connection 对象的不同方式: + +``` + //Oracle8/8i/9i数据库(thin模式) + String url="jdbc:oracle:thin:@localhost:1521:orcl"; + String user="scott"; + String password="tiger"; + Connection conn=DriverManager.getConnection(url,user,password); + + //Sql Server7.0/2000/2005/2008数据库 + String url="jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=pubs"; + String user="sa"; + String password=""; + Connection conn=DriverManager.getConnection(url,user,password); + + //DB2数据库 + String url="jdbc:db2://localhost:5000/sample"; + String user="amdin" + String password=-""; + Connection conn=DriverManager.getConnection(url,user,password); + +//MySQL数据库 +String url="jdbc:mysql://localhost:3306/testDB?user=root&password=root&useUnicode=true&characterEncoding=gb2312"; +Connection conn=DriverManager.getConnection(url); +``` + +3. 执行 SQL 语句   +数据库连接建立好之后,接下来就是一些准备工作和执行 sql 语句了,准备工作要做的就是建立 Statement 对象 PreparedStatement 对象,例如: + +``` + //建立Statement对象 + Statement stmt=conn.createStatement(); + //建立PreparedStatement对象 + String sql="select * from user where userName=? and password=?"; + PreparedStatement pstmt=Conn.prepareStatement(sql); + pstmt.setString(1,"admin"); + pstmt.setString(2,"liubin"); +做好准备工作之后就可以执行sql语句了,执行sql语句: +String sql="select * from users"; +ResultSet rs=stmt.executeQuery(sql); +//执行动态SQL查询 +ResultSet rs=pstmt.executeQuery(); +//执行insert update delete等语句,先定义sql +stmt.executeUpdate(sql); +``` + +4. 处理结果集   +访问结果记录集 ResultSet 对象。例如: + +``` + while(rs.next) + { + out.println("你的第一个字段内容为:"+rs.getString("Name")); + out.println("你的第二个字段内容为:"+rs.getString(2)); + } +``` + +5. 关闭数据库 +依次将 ResultSet、Statement、PreparedStatement、Connection 对象关闭,释放所占用的资源.例如: + +``` + rs.close(); + stmt.clost(); + pstmt.close(); + con.close(); +``` + +二:JDBC 事务 +什么是事务: +首先,说说什么事务。我认为事务,就是一组操作数据库的动作集合。 +事务是现代数据库理论中的核心概念之一。如果一组处理步骤或者全部发生或者一步也不执行,我们称该组处理步骤为一个事务。当所有的步骤像一个操 作一样被完整地执行,我们称该事务被提交。由于其中的一部分或多步执行失败,导致没有步骤被提交,则事务必须回滚到最初的系统状态。 +事务必须服从 ISO/IEC 所制定的 ACID 原则。ACID 是原子性(atomicity)、一致性(consistency)、隔离性 (isolation)和持久性(durability)的缩写。事务的原子性表示事务执行过程中的任何失败都将导致事务所做的任何修改失效。一致性表示 当事务执行失败时,所有被该事务影响的数据都应该恢复到事务执行前的状态。隔离性表示在事务执行过程中对数据的修改,在事务提交之前对其他事务不可见。持久性表示当系统或介质发生故障时,确保已提交事务的更新不能丢失。持久性通过数据库备份和恢复来保证。 +JDBC 事务是用 Connection 对象控制的。JDBC Connection 接口( java.sql.Connection )提供了两种事务模式:自动提交和手工提交。 java.sql.Connection 提供了以下控制事务的方法: + +``` +public void setAutoCommit(boolean) +public boolean getAutoCommit() +public void commit() +public void rollback() +``` + +使用 JDBC 事务界定时,您可以将多个 SQL 语句结合到一个事务中。JDBC 事务的一个缺点是事务的范围局限于一个数据库连接。一个 JDBC 事务不能跨越多个数据库。 +三:java 操作数据库连接池 +在总结 java 操作数据库连接池发现一篇很好的文章,所以就不做具体总结了,直接上地址: +http://www.blogjava.net/chunkyo/archive/2007/01/16/94266.html +最后附一段比较经典的代码吧: + +``` +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Enumeration; +import java.util.Vector; +public class ConnectionPool { +private String jdbcDriver = ""; // 数据库驱动 +private String dbUrl = ""; // 数据 URL +private String dbUsername = ""; // 数据库用户名 +private String dbPassword = ""; // 数据库用户密码 +private String testTable = ""; // 测试连接是否可用的测试表名,默认没有测试表 +private int initialConnections = 10; // 连接池的初始大小 +private int incrementalConnections = 5;// 连接池自动增加的大小 +private int maxConnections = 50; // 连接池最大的大小 +private Vector connections = null; // 存放连接池中数据库连接的向量 , 初始时为 null + +// 它中存放的对象为 PooledConnection 型 + +/** +* 构造函数 +* +* @param jdbcDriver String JDBC 驱动类串 +* @param dbUrl String 数据库 URL +* @param dbUsername String 连接数据库用户名 +* @param dbPassword String 连接数据库用户的密码 +* +*/ + +public ConnectionPool(String jdbcDriver,String dbUrl,String dbUsername,String dbPassword) { + this.jdbcDriver = jdbcDriver; + this.dbUrl = dbUrl; + this.dbUsername = dbUsername; + this.dbPassword = dbPassword; +} + +/** + +* 返回连接池的初始大小 +* +* @return 初始连接池中可获得的连接数量 +*/ +public int getInitialConnections() { + + return this.initialConnections; +} + +/** + +* 设置连接池的初始大小 + +* + +* @param 用于设置初始连接池中连接的数量 + +*/ + +public void setInitialConnections(int initialConnections) { + this.initialConnections = initialConnections; +} + +/** + +* 返回连接池自动增加的大小 、 +* +* @return 连接池自动增加的大小 +*/ +public int getIncrementalConnections() { + + return this.incrementalConnections; + +} + +/** +* 设置连接池自动增加的大小 +* @param 连接池自动增加的大小 +*/ + +public void setIncrementalConnections(int incrementalConnections) { + + this.incrementalConnections = incrementalConnections; + +} + +/** +* 返回连接池中最大的可用连接数量 +* @return 连接池中最大的可用连接数量 +*/ + +public int getMaxConnections() { + return this.maxConnections; +} + +/** + +* 设置连接池中最大可用的连接数量 + +* + +* @param 设置连接池中最大可用的连接数量值 + +*/ + +public void setMaxConnections(int maxConnections) { + + this.maxConnections = maxConnections; + +} + +/** + +* 获取测试数据库表的名字 +* +* @return 测试数据库表的名字 +*/ +public String getTestTable() { + + return this.testTable; + +} + +/** +* 设置测试表的名字 +* @param testTable String 测试表的名字 +*/ +public void setTestTable(String testTable) { + this.testTable = testTable; +} + +/** + +* +* 创建一个数据库连接池,连接池中的可用连接的数量采用类成员 +* initialConnections 中设置的值 +*/ +public synchronized void createPool() throws Exception { + + // 确保连接池没有创建 + + // 如果连接池己经创建了,保存连接的向量 connections 不会为空 + + if (connections != null) { + + return; // 如果己经创建,则返回 + + } + + // 实例化 JDBC Driver 中指定的驱动类实例 + + Driver driver = (Driver) (Class.forName(this.jdbcDriver).newInstance()); + + DriverManager.registerDriver(driver); // 注册 JDBC 驱动程序 + + // 创建保存连接的向量 , 初始时有 0 个元素 + + connections = new Vector(); + + // 根据 initialConnections 中设置的值,创建连接。 + + createConnections(this.initialConnections); + + System.out.println(" 数据库连接池创建成功! "); + +} + +/** + +* 创建由 numConnections 指定数目的数据库连接 , 并把这些连接 + +* 放入 connections 向量中 +* +* @param numConnections 要创建的数据库连接的数目 +*/ +@SuppressWarnings("unchecked") +private void createConnections(int numConnections) throws SQLException { + + // 循环创建指定数目的数据库连接 + + for (int x = 0; x < numConnections; x++) { + + // 是否连接池中的数据库连接的数量己经达到最大?最大值由类成员 maxConnections + + // 指出,如果 maxConnections 为 0 或负数,表示连接数量没有限制。 + + // 如果连接数己经达到最大,即退出。 + + if (this.maxConnections > 0 && this.connections.size() >= this.maxConnections) { + + break; + + } + + //add a new PooledConnection object to connections vector + + // 增加一个连接到连接池中(向量 connections 中) + + try{ + + connections.addElement(new PooledConnection(newConnection())); + + }catch(SQLException e){ + + System.out.println(" 创建数据库连接失败! "+e.getMessage()); + + throw new SQLException(); + + } + + System.out.println(" 数据库连接己创建 ......"); + + } +} + +/** + +* 创建一个新的数据库连接并返回它 +* +* @return 返回一个新创建的数据库连接 +*/ +private Connection newConnection() throws SQLException { + + // 创建一个数据库连接 + + Connection conn = DriverManager.getConnection(dbUrl, dbUsername, dbPassword); + + // 如果这是第一次创建数据库连接,即检查数据库,获得此数据库允许支持的 + + // 最大客户连接数目 + + //connections.size()==0 表示目前没有连接己被创建 + + if (connections.size() == 0) { + + DatabaseMetaData metaData = conn.getMetaData(); + + int driverMaxConnections = metaData.getMaxConnections(); + + // 数据库返回的 driverMaxConnections 若为 0 ,表示此数据库没有最大 + + // 连接限制,或数据库的最大连接限制不知道 + + //driverMaxConnections 为返回的一个整数,表示此数据库允许客户连接的数目 + + // 如果连接池中设置的最大连接数量大于数据库允许的连接数目 , 则置连接池的最大 + + // 连接数目为数据库允许的最大数目 + + if (driverMaxConnections > 0 && this.maxConnections > driverMaxConnections) { + + this.maxConnections = driverMaxConnections; + + } + } + return conn; // 返回创建的新的数据库连接 + +} + +/** + +* 通过调用 getFreeConnection() 函数返回一个可用的数据库连接 , + +* 如果当前没有可用的数据库连接,并且更多的数据库连接不能创 + +* 建(如连接池大小的限制),此函数等待一会再尝试获取。 + +* + +* @return 返回一个可用的数据库连接对象 + +*/ + +public synchronized Connection getConnection() throws SQLException { + + // 确保连接池己被创建 + + if (connections == null) { + + return null; // 连接池还没创建,则返回 null + + } + + Connection conn = getFreeConnection(); // 获得一个可用的数据库连接 + + // 如果目前没有可以使用的连接,即所有的连接都在使用中 + + while (conn == null){ + + // 等一会再试 + + wait(250); + + conn = getFreeConnection(); // 重新再试,直到获得可用的连接,如果 + + //getFreeConnection() 返回的为 null + + // 则表明创建一批连接后也不可获得可用连接 + + } + + return conn;// 返回获得的可用的连接 +} + +/** + +* 本函数从连接池向量 connections 中返回一个可用的的数据库连接,如果 + +* 当前没有可用的数据库连接,本函数则根据 incrementalConnections 设置 + +* 的值创建几个数据库连接,并放入连接池中。 + +* 如果创建后,所有的连接仍都在使用中,则返回 null + +* @return 返回一个可用的数据库连接 + +*/ + +private Connection getFreeConnection() throws SQLException { + + // 从连接池中获得一个可用的数据库连接 + + Connection conn = findFreeConnection(); + + if (conn == null) { + + // 如果目前连接池中没有可用的连接 + + // 创建一些连接 + + createConnections(incrementalConnections); + + // 重新从池中查找是否有可用连接 + + conn = findFreeConnection(); + + if (conn == null) { + + // 如果创建连接后仍获得不到可用的连接,则返回 null + + return null; + + } + + } + + return conn; + +} + +/** + +* 查找连接池中所有的连接,查找一个可用的数据库连接, + +* 如果没有可用的连接,返回 null + +* + +* @return 返回一个可用的数据库连接 + +*/ + +private Connection findFreeConnection() throws SQLException { + + Connection conn = null; + + PooledConnection pConn = null; + + // 获得连接池向量中所有的对象 + + Enumeration enumerate = connections.elements(); + + // 遍历所有的对象,看是否有可用的连接 + + while (enumerate.hasMoreElements()) { + + pConn = (PooledConnection) enumerate.nextElement(); + + if (!pConn.isBusy()) { + + // 如果此对象不忙,则获得它的数据库连接并把它设为忙 + + conn = pConn.getConnection(); + + pConn.setBusy(true); + + // 测试此连接是否可用 + + if (!testConnection(conn)) { + + // 如果此连接不可再用了,则创建一个新的连接, + + // 并替换此不可用的连接对象,如果创建失败,返回 null + + try{ + + conn = newConnection(); + + }catch(SQLException e){ + + System.out.println(" 创建数据库连接失败! "+e.getMessage()); + + return null; + + } + + pConn.setConnection(conn); + + } + + break; // 己经找到一个可用的连接,退出 + + } + + } + + return conn;// 返回找到到的可用连接 + +} + +/** + +* 测试一个连接是否可用,如果不可用,关掉它并返回 false + +* 否则可用返回 true + +* + +* @param conn 需要测试的数据库连接 + +* @return 返回 true 表示此连接可用, false 表示不可用 + +*/ + +private boolean testConnection(Connection conn) { + + try { + + // 判断测试表是否存在 + + if (testTable.equals("")) { + + // 如果测试表为空,试着使用此连接的 setAutoCommit() 方法 + + // 来判断连接否可用(此方法只在部分数据库可用,如果不可用 , + + // 抛出异常)。注意:使用测试表的方法更可靠 + + conn.setAutoCommit(true); + + } else {// 有测试表的时候使用测试表测试 + + //check if this connection is valid + + Statement stmt = conn.createStatement(); + + stmt.execute("select count(*) from " + testTable); + + } + + } catch (SQLException e) { + + // 上面抛出异常,此连接己不可用,关闭它,并返回 false; + + closeConnection(conn); + + return false; + + } + + // 连接可用,返回 true + + return true; + +} + +/** + +* 此函数返回一个数据库连接到连接池中,并把此连接置为空闲。 + +* 所有使用连接池获得的数据库连接均应在不使用此连接时返回它。 + +* + +* @param 需返回到连接池中的连接对象 + +*/ + +public void returnConnection(Connection conn) { + + // 确保连接池存在,如果连接没有创建(不存在),直接返回 + + if (connections == null) { + + System.out.println(" 连接池不存在,无法返回此连接到连接池中 !"); + + return; + + } + + PooledConnection pConn = null; + + Enumeration enumerate = connections.elements(); + + // 遍历连接池中的所有连接,找到这个要返回的连接对象 + + while (enumerate.hasMoreElements()) { + + pConn = (PooledConnection) enumerate.nextElement(); + + // 先找到连接池中的要返回的连接对象 + + if (conn == pConn.getConnection()) { + + // 找到了 , 设置此连接为空闲状态 + + pConn.setBusy(false); + + break; + + } + + } + +} + +/** + +* 刷新连接池中所有的连接对象 + +* + +*/ + +public synchronized void refreshConnections() throws SQLException { + + // 确保连接池己创新存在 + + if (connections == null) { + + System.out.println(" 连接池不存在,无法刷新 !"); + + return; + + } + + PooledConnection pConn = null; + + Enumeration enumerate = connections.elements(); + + while (enumerate.hasMoreElements()) { + + // 获得一个连接对象 + + pConn = (PooledConnection) enumerate.nextElement(); + + // 如果对象忙则等 5 秒 ,5 秒后直接刷新 + + if (pConn.isBusy()) { + + wait(5000); // 等 5 秒 + + } + + // 关闭此连接,用一个新的连接代替它。 + + closeConnection(pConn.getConnection()); + + pConn.setConnection(newConnection()); + + pConn.setBusy(false); + + } + +} + +/** + +* 关闭连接池中所有的连接,并清空连接池。 + +*/ + +public synchronized void closeConnectionPool() throws SQLException { + + // 确保连接池存在,如果不存在,返回 + + if (connections == null) { + + System.out.println(" 连接池不存在,无法关闭 !"); + + return; + + } + + PooledConnection pConn = null; + + Enumeration enumerate = connections.elements(); + + while (enumerate.hasMoreElements()) { + + pConn = (PooledConnection) enumerate.nextElement(); + + // 如果忙,等 5 秒 + + if (pConn.isBusy()) { + + wait(5000); // 等 5 秒 + + } + + //5 秒后直接关闭它 + + closeConnection(pConn.getConnection()); + + // 从连接池向量中删除它 + + connections.removeElement(pConn); + + } + + // 置连接池为空 + + connections = null; + +} + +/** + +* 关闭一个数据库连接 + +* + +* @param 需要关闭的数据库连接 + +*/ + +private void closeConnection(Connection conn) { + + try { + + conn.close(); + + }catch (SQLException e) { + + System.out.println(" 关闭数据库连接出错: "+e.getMessage()); + + } + +} + +/** + +* 使程序等待给定的毫秒数 + +* + +* @param 给定的毫秒数 + +*/ + +private void wait(int mSeconds) { + + try { + + Thread.sleep(mSeconds); + + } catch (InterruptedException e) { + + } + +} + +/** + +* + +* 内部使用的用于保存连接池中连接对象的类 + +* 此类中有两个成员,一个是数据库的连接,另一个是指示此连接是否 + +* 正在使用的标志。 + +*/ + +class PooledConnection { + + Connection connection = null;// 数据库连接 + + boolean busy = false; // 此连接是否正在使用的标志,默认没有正在使用 + + // 构造函数,根据一个 Connection 构告一个 PooledConnection 对象 + + public PooledConnection(Connection connection) { + + this.connection = connection; + + } + + // 返回此对象中的连接 + + public Connection getConnection() { + + return connection; + + } + + // 设置此对象的,连接 + + public void setConnection(Connection connection) { + + this.connection = connection; + + } + + // 获得对象连接是否忙 + + public boolean isBusy() { + + return busy; + + } + + // 设置对象的连接正在忙 + + public void setBusy(boolean busy) { + + this.busy = busy; + + } + +} + +} + +======================================= + +这个例子是根据POSTGRESQL数据库写的, +请用的时候根据实际的数据库调整。 + +调用方法如下: + +① ConnectionPool connPool + = new ConnectionPool("org.postgresql.Driver" + ,"jdbc:postgresql://dbURI:5432/DBName" + ,"postgre" + ,"postgre"); + +② connPool .createPool(); +  Connection conn = connPool .getConnection(); +``` \ No newline at end of file diff --git a/design-pattern.md b/design-pattern.md new file mode 100644 index 0000000..bac95d0 --- /dev/null +++ b/design-pattern.md @@ -0,0 +1,2223 @@ +# 常见设计模式 + +# Java 开发中的23种设计模式详解(转) + +**设计模式(Design Patterns)** ——可复用面向对象软件的基础 + +设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。本章系** Java 之美[从菜鸟到高手演变]系列**之设计模式,我们会以理论与实践相结合的方式来进行本章的学习,希望广大程序爱好者,学好设计模式,做一个优秀的软件工程师! + +**企业级项目实战(带源码)地址:** http://zz563143188.iteye.com/blog/1825168 + +**23种模式 java 实现源码下载地址:** http://pan.baidu.com/share/link?shareid=372668&uk=4076915866#dir/path=%2F%E5%AD%A6%E4%B9%A0%E6%96%87%E4%BB%B6 + +**一、设计模式的分类** + +总体来说设计模式分为三大类: + +创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。 + +结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。 + +行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。 + +其实还有两类:并发型模式和线程池模式。用一个图片来整体描述一下: + +![](images/3.jpg) + +**二、设计模式的六大原则** + +**1、开闭原则(Open Close Principle)** + +开闭原则就是说**对扩展开放,对修改关闭**。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。 + +**2、里氏代换原则(Liskov Substitution Principle)** + +里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP 是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。—— From Baidu 百科 + +**3、依赖倒转原则(Dependence Inversion Principle)** + +这个是开闭原则的基础,具体内容:真对接口编程,依赖于抽象而不依赖于具体。 + +**4、接口隔离原则(Interface Segregation Principle)** + +这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。 + +**5、迪米特法则(最少知道原则)(Demeter Principle)** + +为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。 + +**6、合成复用原则(Composite Reuse Principle)** + +原则是尽量使用合成/聚合的方式,而不是使用继承。 + +**三、Java 的23中设计模式** + +从这一块开始,我们详细介绍 Java 中23种设计模式的概念,应用场景等情况,并结合他们的特点及设计模式的原则进行分析。 + +**1、工厂方法模式(Factory Method)** + +工厂方法模式分为三种: + +***11、普通工厂模式***,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。首先看下关系图: + +![](images/14.png) + +举例如下:(我们举一个发送邮件和短信的例子) + +首先,创建二者的共同接口: + +``` +[java] view plaincopy +public interface Sender { + public void Send(); +} +``` + +其次,创建实现类: + +``` +[java] view plaincopy +public class MailSender implements Sender { + @Override + public void Send() { + System.out.println("this is mailsender!"); + } +} +[java] view plaincopy +public class SmsSender implements Sender { + + @Override + public void Send() { + System.out.println("this is sms sender!"); + } +} +``` +最后,建工厂类: + +``` +[java] view plaincopy +public class SendFactory { + + public Sender produce(String type) { + if ("mail".equals(type)) { + return new MailSender(); + } else if ("sms".equals(type)) { + return new SmsSender(); + } else { + System.out.println("请输入正确的类型!"); + return null; + } + } +} +``` + +我们来测试下: + +``` +public class FactoryTest { + + public static void main(String[] args) { + SendFactory factory = new SendFactory(); + Sender sender = factory.produce("sms"); + sender.Send(); + } +} +``` + +输出:this is sms sender! + +***22、多个工厂方法模式***,是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。关系图: + +![](images/4.jpg) + +将上面的代码做下修改,改动下 SendFactory 类就行,如下: + +``` +[java] view plaincopypublic class SendFactory { + public Sender produceMail(){ + return new MailSender(); + } + + public Sender produceSms(){ + return new SmsSender(); + } +} +``` + +测试类如下: + +``` +[java] view plaincopy +public class FactoryTest { + + public static void main(String[] args) { + SendFactory factory = new SendFactory(); + Sender sender = factory.produceMail(); + sender.Send(); + } +} +``` + +输出:this is mailsender! + +***33、静态工厂方法模式***,将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。 + +``` +[java] view plaincopy +public class SendFactory { + + public static Sender produceMail(){ + return new MailSender(); + } + + public static Sender produceSms(){ + return new SmsSender(); + } +} +[java] view plaincopy +public class FactoryTest { + + public static void main(String[] args) { + Sender sender = SendFactory.produceMail(); + sender.Send(); + } +} +``` + +输出:this is mailsender! + +总体来说,工厂模式适合:凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。在以上的三种模式中,第一种如果传入的字符串有误,不能正确创建对象,第三种相对于第二种,不需要实例化工厂类,所以,大多数情况下,我们会选用第三种——静态工厂方法模式。 + +**2、抽象工厂模式(Abstract Factory)** + +工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。因为抽象工厂不太好理解,我们先看看图,然后就和代码,就比较容易理解。 + +![](images/5.jpg) + +请看例子: + +``` +[java] view plaincopy +public interface Sender { + public void Send(); +} +``` + +``` +两个实现类: + +[java] view plaincopy +public class MailSender implements Sender { + @Override + public void Send() { + System.out.println("this is mailsender!"); + } +} +[java] view plaincopy +public class SmsSender implements Sender { + + @Override + public void Send() { + System.out.println("this is sms sender!"); + } +} +``` + +两个工厂类: + +``` +[java] view plaincopy +public class SendMailFactory implements Provider { + + @Override + public Sender produce(){ + return new MailSender(); + } +} +[java] view plaincopy +public class SendSmsFactory implements Provider{ + + @Override + public Sender produce() { + return new SmsSender(); + } +} +``` + +在提供一个接口: + +``` +[java] view plaincopy +public interface Provider { + public Sender produce(); +} +``` + +测试类: + +``` +[java] view plaincopy +public class Test { + + public static void main(String[] args) { + Provider provider = new SendMailFactory(); + Sender sender = provider.produce(); + sender.Send(); + } +} +``` + +其实这个模式的好处就是,如果你现在想增加一个功能:发及时信息,则只需做一个实现类,实现 Sender 接口,同时做一个工厂类,实现 Provider 接口,就 OK 了,无需去改动现成的代码。这样做,拓展性较好! + +**3、单例模式(Singleton)** + +单例对象(Singleton)是一种常用的设计模式。在 Java 应用中,单例对象能保证在一个 JVM 中,该对象只有一个实例存在。这样的模式有几个好处: + +1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。 + +2、省去了 new 操作符,降低了系统内存的使用频率,减轻 GC 压力。 + +3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。 + +首先我们写一个简单的单例类: + +``` +[java] view plaincopy +public class Singleton { + + /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */ + private static Singleton instance = null; + + /* 私有构造方法,防止被实例化 */ + private Singleton() { + } + + /* 静态工程方法,创建实例 */ + public static Singleton getInstance() { + if (instance == null) { + instance = new Singleton(); + } + return instance; + } + + /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */ + public Object readResolve() { + return instance; + } +} +``` + +这个类可以满足基本要求,但是,像这样毫无线程安全保护的类,如果我们把它放入多线程的环境下,肯定就会出现问题了,如何解决?我们首先会想到对 getInstance 方法加 synchronized 关键字,如下: + +``` +[java] view plaincopy +public static synchronized Singleton getInstance() { + if (instance == null) { + instance = new Singleton(); + } + return instance; + } +``` + +但是,synchronized 关键字锁住的是这个对象,这样的用法,在性能上会有所下降,因为每次调用 getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,所以,这个地方需要改进。我们改成下面这个: + +``` +[java] view plaincopy +public static Singleton getInstance() { + if (instance == null) { + synchronized (instance) { + if (instance == null) { + instance = new Singleton(); + } + } + } + return instance; + } +``` + +似乎解决了之前提到的问题,将 synchronized 关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在 instance 为 null,并创建对象的时候才需要加锁,性能有一定的提升。但是,这样的情况,还是有可能有问题的,看下面的情况:在 Java 指令中创建对象和赋值操作是分开进行的,也就是说 instance = new Singleton();语句是分两步执行的。但是 JVM 并不保证这两个操作的先后顺序,也就是说有可能 JVM 会为新的 Singleton 实例分配空间,然后直接赋值给 instance 成员,然后再去初始化这个 Singleton 实例。这样就可能出错了,我们以 A、B 两个线程为例: + +a>A、B 线程同时进入了第一个 if 判断 + +b>A 首先进入 synchronized 块,由于 instance 为 null,所以它执行 instance = new Singleton(); + +c> 由于 JVM 内部的优化机制,JVM 先画出了一些分配给 Singleton 实例的空白内存,并赋值给 instance 成员(注意此时 JVM 没有开始初始化这个实例),然后 A 离开了 synchronized 块。 + +d>B 进入 synchronized 块,由于 instance 此时不是 null,因此它马上离开了synchronized 块并将结果返回给调用该方法的程序。 + +e> 此时 B 线程打算使用 Singleton 实例,却发现它没有被初始化,于是错误发生了。 + +所以程序还是有可能发生错误,其实程序在运行过程是很复杂的,从这点我们就可以看出,尤其是在写多线程环境下的程序更有难度,有挑战性。我们对该程序做进一步优化: + +``` +[java] view plaincopy +private static class SingletonFactory{ + private static Singleton instance = new Singleton(); + } + public static Singleton getInstance(){ + return SingletonFactory.instance; + } +``` + +实际情况是,单例模式使用内部类来维护单例的实现,JVM 内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用 getInstance 的时候,JVM 能够帮我们保证 instance 只被创建一次,并且会保证把赋值给 instance 的内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。这样我们暂时总结一个完美的单例模式: + +``` +[java] view plaincopy +public class Singleton { + + /* 私有构造方法,防止被实例化 */ + private Singleton() { + } + + /* 此处使用一个内部类来维护单例 */ + private static class SingletonFactory { + private static Singleton instance = new Singleton(); + } + + /* 获取实例 */ + public static Singleton getInstance() { + return SingletonFactory.instance; + } + + /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */ + public Object readResolve() { + return getInstance(); + } +} +``` + +其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况,选择最适合自己应用场景的实现方法。也有人这样实现:因为我们只需要在创建类的时候进行同步,所以只要将创建和 getInstance()分开,单独为创建加 synchronized 关键字,也是可以的: + +``` +[java] view plaincopy +public class SingletonTest { + + private static SingletonTest instance = null; + + private SingletonTest() { + } + + private static synchronized void syncInit() { + if (instance == null) { + instance = new SingletonTest(); + } + } + + public static SingletonTest getInstance() { + if (instance == null) { + syncInit(); + } + return instance; + } +} +``` + +考虑性能的话,整个程序只需创建一次实例,所以性能也不会有什么影响。 + +**补充:采用"影子实例"的办法为单例对象的属性同步更新** + +``` +[java] view plaincopy +public class SingletonTest { + + private static SingletonTest instance = null; + private Vector properties = null; + + public Vector getProperties() { + return properties; + } + + private SingletonTest() { + } + + private static synchronized void syncInit() { + if (instance == null) { + instance = new SingletonTest(); + } + } + + public static SingletonTest getInstance() { + if (instance == null) { + syncInit(); + } + return instance; + } + + public void updateProperties() { + SingletonTest shadow = new SingletonTest(); + properties = shadow.getProperties(); + } +} +``` + +通过单例模式的学习告诉我们: + +1、单例模式理解起来简单,但是具体实现起来还是有一定的难度。 + +2、synchronized 关键字锁定的是对象,在用的时候,一定要在恰当的地方使用(注意需要使用锁的对象和过程,可能有的时候并不是整个对象及整个过程都需要锁)。 + +到这儿,单例模式基本已经讲完了,结尾处,笔者突然想到另一个问题,就是采用类的静态方法,实现单例模式的效果,也是可行的,此处二者有什么不同? + +首先,静态类不能实现接口。(从类的角度说是可以的,但是那样就破坏了静态了。因为接口中不允许有 static 修饰的方法,所以即使实现了也是非静态的) + +其次,单例可以被延迟初始化,静态类一般在第一次加载是初始化。之所以延迟加载,是因为有些类比较庞大,所以延迟加载有助于提升性能。 + +再次,单例类可以被继承,他的方法可以被覆写。但是静态类内部方法都是 static,无法被覆写。 + +最后一点,单例类比较灵活,毕竟从实现上只是一个普通的 Java 类,只要满足单例的基本需求,你可以在里面随心所欲的实现一些其它功能,但是静态类不行。从上面这些概括中,基本可以看出二者的区别,但是,从另一方面讲,我们上面最后实现的那个单例模式,内部就是用一个静态类来实现的,所以,二者有很大的关联,只是我们考虑问题的层面不同罢了。两种思想的结合,才能造就出完美的解决方案,就像 HashMap 采用数组+链表来实现一样,其实生活中很多事情都是这样,单用不同的方法来处理问题,总是有优点也有缺点,最完美的方法是,结合各个方法的优点,才能最好的解决问题! + +**4、建造者模式(Builder)** + +工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性,其实建造者模式就是前面抽象工厂模式和最后的 Test 结合起来得到的。我们看一下代码: + +还和前面一样,一个 Sender 接口,两个实现类 MailSender 和 SmsSender。最后,建造者类如下: + +``` +[java] view plaincopy +public class Builder { + + private List list = new ArrayList(); + + public void produceMailSender(int count){ + for(int i=0; i children = new Vector(); + + public TreeNode(String name){ + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public TreeNode getParent() { + return parent; + } + + public void setParent(TreeNode parent) { + this.parent = parent; + } + + //添加孩子节点 + public void add(TreeNode node){ + children.add(node); + } + + //删除孩子节点 + public void remove(TreeNode node){ + children.remove(node); + } + + //取得孩子节点 + public Enumeration getChildren(){ + return children.elements(); + } +} +[java] view plaincopy +public class Tree { + + TreeNode root = null; + + public Tree(String name) { + root = new TreeNode(name); + } + + public static void main(String[] args) { + Tree tree = new Tree("A"); + TreeNode nodeB = new TreeNode("B"); + TreeNode nodeC = new TreeNode("C"); + + nodeB.add(nodeC); + tree.root.add(nodeB); + System.out.println("build the tree finished!"); + } +} +``` + +使用场景:将多个对象组合在一起进行操作,常用于表示树形结构中,例如二叉树,数等。 + +**12、享元模式(Flyweight)** + +享元模式的主要目的是实现对象的共享,即共享池,当系统中对象多的时候可以减少内存的开销,通常与工厂模式一起使用。 + +![](images/16.jpg) + +FlyWeightFactory 负责创建和管理享元单元,当一个客户端请求时,工厂需要检查当前对象池中是否有符合条件的对象,如果有,就返回已经存在的对象,如果没有,则创建一个新对象,FlyWeight 是超类。一提到共享池,我们很容易联想到 Java 里面的 JDBC 连接池,想想每个连接的特点,我们不难总结出:适用于作共享的一些个对象,他们有一些共有的属性,就拿数据库连接池来说,url、driverClassName、username、password 及 dbname,这些属性对于每个连接来说都是一样的,所以就适合用享元模式来处理,建一个工厂类,将上述类似属性作为内部数据,其它的作为外部数据,在方法调用时,当做参数传进来,这样就节省了空间,减少了实例的数量。 + +看个例子: + +![](images/17.jpg) + +看下数据库连接池的代码: + +``` +[java] view plaincopy +public class ConnectionPool { + + private Vector pool; + + /*公有属性*/ + private String url = "jdbc:mysql://localhost:3306/test"; + private String username = "root"; + private String password = "root"; + private String driverClassName = "com.mysql.jdbc.Driver"; + + private int poolSize = 100; + private static ConnectionPool instance = null; + Connection conn = null; + + /*构造方法,做一些初始化工作*/ + private ConnectionPool() { + pool = new Vector(poolSize); + + for (int i = 0; i < poolSize; i++) { + try { + Class.forName(driverClassName); + conn = DriverManager.getConnection(url, username, password); + pool.add(conn); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + /* 返回连接到连接池 */ + public synchronized void release() { + pool.add(conn); + } + + /* 返回连接池中的一个数据库连接 */ + public synchronized Connection getConnection() { + if (pool.size() > 0) { + Connection conn = pool.get(0); + pool.remove(conn); + return conn; + } else { + return null; + } + } +} +``` + +通过连接池的管理,实现了数据库连接的共享,不需要每一次都重新创建连接,节省了数据库重新创建的开销,提升了系统的性能!本章讲解了7种结构型模式,因为篇幅的问题,剩下的11种行为型模式, + +本章是关于设计模式的最后一讲,会讲到第三种设计模式——行为型模式,共11种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。这段时间一直在写关于设计模式的东西,终于写到一半了,写博文是个很费时间的东西,因为我得为读者负责,不论是图还是代码还是表述,都希望能尽量写清楚,以便读者理解,我想不论是我还是读者,都希望看到高质量的博文出来,从我本人出发,我会一直坚持下去,不断更新,源源动力来自于读者朋友们的不断支持,我会尽自己的努力,写好每一篇文章!希望大家能不断给出意见和建议,共同打造完美的博文! + +先来张图,看看这11中模式的关系: + +第一类:通过父类与子类的关系进行实现。第二类:两个类之间。第三类:类的状态。第四类:通过中间类 + +![](images/18.jpg) + +**13、策略模式(strategy)** + +策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。需要设计一个接口,为一系列实现类提供统一的方法,多个实现类实现该接口,设计一个抽象类(可有可无,属于辅助类),提供辅助函数,关系图如下: + +![](images/19.jpg) + +图中 ICalculator 提供同意的方法, +AbstractCalculator 是辅助类,提供辅助方法,接下来,依次实现下每个类: + +首先统一接口: + +``` +[java] view plaincopy +public interface ICalculator { + public int calculate(String exp); +} +``` + +辅助类: + +``` +[java] view plaincopy +public abstract class AbstractCalculator { + + public int[] split(String exp,String opt){ + String array[] = exp.split(opt); + int arrayInt[] = new int[2]; + arrayInt[0] = Integer.parseInt(array[0]); + arrayInt[1] = Integer.parseInt(array[1]); + return arrayInt; + } +} +``` + +三个实现类: + +``` +[java] view plaincopy +public class Plus extends AbstractCalculator implements ICalculator { + + @Override + public int calculate(String exp) { + int arrayInt[] = split(exp,"\\+"); + return arrayInt[0]+arrayInt[1]; + } +} +[java] view plaincopy +public class Minus extends AbstractCalculator implements ICalculator { + + @Override + public int calculate(String exp) { + int arrayInt[] = split(exp,"-"); + return arrayInt[0]-arrayInt[1]; + } + +} +[java] view plaincopy +public class Multiply extends AbstractCalculator implements ICalculator { + + @Override + public int calculate(String exp) { + int arrayInt[] = split(exp,"\\*"); + return arrayInt[0]*arrayInt[1]; + } +} +``` + +简单的测试类: + +``` +[java] view plaincopy +public class StrategyTest { + + public static void main(String[] args) { + String exp = "2+8"; + ICalculator cal = new Plus(); + int result = cal.calculate(exp); + System.out.println(result); + } +} +``` + +输出:10 + +策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。因此,策略模式多用在算法决策系统中,外部用户只需要决定用哪个算法即可。 + +**14、模板方法模式(Template Method)** + +解释一下模板方法模式,就是指:一个抽象类中,有一个主方法,再定义 1...n 个方法,可以是抽象的,也可以是实际的方法,定义一个类,继承该抽象类,重写抽象方法,通过调用抽象类,实现对子类的调用,先看个关系图: + +![](images/20.jpg) + +就是在 AbstractCalculator 类中定义一个主方法 calculate,calculate()调用 spilt()等,Plus 和 Minus 分别继承 AbstractCalculator 类,通过对 AbstractCalculator 的调用实现对子类的调用,看下面的例子: + +``` +[java] view plaincopy +public abstract class AbstractCalculator { + + /*主方法,实现对本类其它方法的调用*/ + public final int calculate(String exp,String opt){ + int array[] = split(exp,opt); + return calculate(array[0],array[1]); + } + + /*被子类重写的方法*/ + abstract public int calculate(int num1,int num2); + + public int[] split(String exp,String opt){ + String array[] = exp.split(opt); + int arrayInt[] = new int[2]; + arrayInt[0] = Integer.parseInt(array[0]); + arrayInt[1] = Integer.parseInt(array[1]); + return arrayInt; + } +} +[java] view plaincopy +public class Plus extends AbstractCalculator { + + @Override + public int calculate(int num1,int num2) { + return num1 + num2; + } +} +``` + +测试类: + +``` +[java] view plaincopy +public class StrategyTest { + + public static void main(String[] args) { + String exp = "8+8"; + AbstractCalculator cal = new Plus(); + int result = cal.calculate(exp, "\\+"); + System.out.println(result); + } +} +``` + +我跟踪下这个小程序的执行过程:首先将 exp 和"\\+"做参数,调用 AbstractCalculator 类里的 calculate(String,String)方法,在 calculate(String,String)里调用同类的 split(),之后再调用 calculate(int ,int)方法,从这个方法进入到子类中,执行完 return num1 + num2 后,将值返回到 AbstractCalculator 类,赋给 result,打印出来。正好验证了我们开头的思路。 + +**15、观察者模式(Observer)** + +包括这个模式在内的接下来的四个模式,都是类和类之间的关系,不涉及到继承,学的时候应该 记得归纳,记得本文最开始的那个图。观察者模式很好理解,类似于邮件订阅和 RSS 订阅,当我们浏览一些博客或 wiki 时,经常会看到 RSS 图标,就这的意思是,当你订阅了该文章,如果后续有更新,会及时通知你。其实,简单来讲就一句话:当一个对象变化时,其它依赖该对象的对象都会收到通知,并且随着变化!对象之间是一种一对多的关系。先来看看关系图: + +![](images/21.jpg) + +我解释下这些类的作用:MySubject 类就是我们的主对象,Observer1 和 Observer2 是依赖于 MySubject 的对象,当 MySubject 变化时,Observer1 和 Observer2 必然变化。AbstractSubject 类中定义着需要监控的对象列表,可以对其进行修改:增加或删除被监控对象,且当 MySubject 变化时,负责通知在列表内存在的对象。我们看实现代码: + +一个 Observer 接口: + +``` +[java] view plaincopy +public interface Observer { + public void update(); +} +``` + +两个实现类: + +``` +[java] view plaincopy +public class Observer1 implements Observer { + + @Override + public void update() { + System.out.println("observer1 has received!"); + } +} +[java] view plaincopy +public class Observer2 implements Observer { + + @Override + public void update() { + System.out.println("observer2 has received!"); + } + +} +``` + +Subject 接口及实现类: + +``` +[java] view plaincopy +public interface Subject { + + /*增加观察者*/ + public void add(Observer observer); + + /*删除观察者*/ + public void del(Observer observer); + + /*通知所有的观察者*/ + public void notifyObservers(); + + /*自身的操作*/ + public void operation(); +} +[java] view plaincopy +public abstract class AbstractSubject implements Subject { + + private Vector vector = new Vector(); + @Override + public void add(Observer observer) { + vector.add(observer); + } + + @Override + public void del(Observer observer) { + vector.remove(observer); + } + + @Override + public void notifyObservers() { + Enumeration enumo = vector.elements(); + while(enumo.hasMoreElements()){ + enumo.nextElement().update(); + } + } +} +[java] view plaincopy +public class MySubject extends AbstractSubject { + + @Override + public void operation() { + System.out.println("update self!"); + notifyObservers(); + } + +} +``` + +测试类: + +``` +[java] view plaincopy +public class ObserverTest { + + public static void main(String[] args) { + Subject sub = new MySubject(); + sub.add(new Observer1()); + sub.add(new Observer2()); + + sub.operation(); + } + +} +``` + +输出: + +``` +update self! +observer1 has received! +observer2 has received! +``` + + 这些东西,其实不难,只是有些抽象,不太容易整体理解,建议读者:**根据关系图,新建项目,自己写代码(或者参考我的代码),按照总体思路走一遍,这样才能体会它的思想,理解起来容易! ** + +**16、迭代子模式(Iterator)** + +顾名思义,迭代器模式就是顺序访问聚集中的对象,一般来说,集合中非常常见,如果对集合类比较熟悉的话,理解本模式会十分轻松。这句话包含两层意思:一是需要遍历的对象,即聚集对象,二是迭代器对象,用于对聚集对象进行遍历访问。我们看下关系图: + +![](images/22.jpg) + +这个思路和我们常用的一模一样,MyCollection 中定义了集合的一些操作,MyIterator 中定义了一系列迭代操作,且持有 Collection 实例,我们来看看实现代码: + +两个接口: + +``` +[java] view plaincopy +public interface Collection { + + public Iterator iterator(); + + /*取得集合元素*/ + public Object get(int i); + + /*取得集合大小*/ + public int size(); +} +[java] view plaincopy +public interface Iterator { + //前移 + public Object previous(); + + //后移 + public Object next(); + public boolean hasNext(); + + //取得第一个元素 + public Object first(); +} +``` + +两个实现: + +``` +[java] view plaincopy +public class MyCollection implements Collection { + + public String string[] = {"A","B","C","D","E"}; + @Override + public Iterator iterator() { + return new MyIterator(this); + } + + @Override + public Object get(int i) { + return string[i]; + } + + @Override + public int size() { + return string.length; + } +} +[java] view plaincopy +public class MyIterator implements Iterator { + + private Collection collection; + private int pos = -1; + + public MyIterator(Collection collection){ + this.collection = collection; + } + + @Override + public Object previous() { + if(pos > 0){ + pos--; + } + return collection.get(pos); + } + + @Override + public Object next() { + if(pos(小明) +# 搭建 Java 开发环境 + +并不是每一台计算机都可以运行 Java 程序,要运行 Java 程序,计算机必须搭建 Java 开发环境,而编写 Java 程序则可以使用任何的文本编辑工具,如最简单的文本编辑工具之一——记事本。 + +**下载 JDK 的具体步骤如下:** + +1、打开浏览器,在浏览器的地址栏里输入如下网址: + +http://www.oracle.com/technetwork/java/javase/downloads/index.html + +进入界面如下: + +![](images/15.png) + +2、点击图中的红色区域 + +![](images/16.png) + +进入界面如下: + +![](images/17.png) + +3、选择适合你系统类型的安装文件,进行下载。 + +**安装 JDK 的具体步骤如下:** + +1、双击下载的 JDK 安装程序,进入安装界面,首先进入安装向导界面,如图: + +![](images/18.png) + +2、单击“下一步”按钮,进入自定义安装界面,如图: + +![](images/19.png) + + +3、选择需要安装的程序,如果需要更改安装位置,可以单击“更改”按钮,选择安装位置。单击“下一步”按钮,开始安装。 + +注:在安装 JDK 时,没有快捷方式,也不需要快捷方式,因此里面的方法和类都是通过程序员编写的程序隐式调用的。 + +**Windows 系统下配置和测试 JDK** + +安装完 JDK 后,需要设置环境变量及测试 JDK 配置是否成功,具体步骤如下: + +1、在“计算机”图标上单击鼠标右键,选择“属性”命令,在打开的“高级系统设置”,对话框中选择“高级”选项卡; + +2、单击“环境变量”按钮,打开“环境变量”对话框,在这里可以添加针对单个用户的“用户变量”和针对所有用户的“系统变量”; + +3、单击“系统变量”栏中的“新建”按钮,弹出“编辑系统变量”对话框,在“变量名”文本框中输入“JAVA_HOME”,在“变量值”文本框中输入 JDK 的安装路径“C:\Program Files\Java\jdk1.6.0_24”,单击“确定”按钮,完成环境变量“JAVA_HOME”的配置。 + +4、在系统变量中,查看 Path 变量,如果不存在,则新建变量 Path,否则选择该变量,单击“环境变量”对话框中的“编辑”按钮,打开“编辑系统变量”对话框,在该对话框的“变量值”文本框的起始位置添加“%JAVA_HOME%\bin;”,单击 “确定”按钮完成环境变量的配置。 + +5、JDK 程序安装和配置完成后,可以测试 JDK 是否能够在计算机上运行,步骤是:选择“开始”--“运行”命令,在打开的“运行”对话框中输入“cmd”命令,确定后将进入到 DOS 环境中,在命令提示符后面直接输入“javac”,按下键,系统会输出 javac 的帮助信息,说明已经成功配置了 JDK,否则需要仔细检查上面步骤的配置是否正确。 + +**Java 程序的开发过程** + +开发 Java 程序总体上可以分为3步: + +1、编写 Java 源文件。Java 源文件是一种文本文件,其扩展名为 .java。(如:X.java) + +2、编译 Java 源文件,也就是将 Java 源文件编译(Compile)成 Java 类文件(扩展名为.class),如:使用“javac.exe”命令将 X.java 文件编译成“X.class”类文件。 + +Java 类文件由字节码构成,所以也可以称为字节码文件,所谓的字节码文件是与平台无关的二进制码,执行时由解释器(java.exe)解释成本地计算机码。一边解释一边执行,解释一句,执行一句。 + +3、运行 Java 程序。Java 程序可以分为 Java Application(Java 应用程序)和 Java Applet(Java 小应用程序)。其中,Java Application 必须通过 Java 解释器来解释执行其字节码文件,Java Applet 必须使用支持它的浏览器(IE 浏览器)运行。 + +# Mac OS X 下搭建 Java 开发环境图解 + +本篇博客介绍如何在 Mac osx 系统下搭建 java 开发环境,有了 java 的开发环境,我们就可以做 Java 相关的开发,Eclipse 和 Android Studio 都是要有 JVM 环境才能运行的,所以本篇就稍微总结一下如何在 Mac osx 下配置 java 环境变量。 + +先来看看笔者的电脑配置: + +![](images/20.png) + +打开终端,查看10.10版本的系统使用的是什么 shell 命令: + +![](images/21.png) + +输出的是 bash,说明是 Bourne shell,是默认的 Unix Shell 命令。 + +下面通过命令行查看笔者的 java 版本: + +![](images/22.png) + +如果你的系统已经安装成功 JDK,通过 java -version 就可以看到相应的 jdk 版本。 + +如果你的电脑还没有安装 JDK 的话,可以到 Oracle 官网下载 jdk + +http://www.oracle.com/technetwork/java/javase/downloads/jdk7-downloads-1880260.html + +笔者下载的1.7版本,目前 jdk 最新版本为1.8。 + +配置 java 环境变量 + +输入 sudo vim etc/profile + +![](images/23.png) + +使用 vi 编辑 profile 文件 + +键入 i,进入插入模式 + +在文件尾部,添加 java 路径 + +JAVA_HOME="/Library/Java/JavaVirtualMachines/jdk1.7.0_75.jdk/Contents/Home/" +CLASS_PATH="$JAVA_HOME/lib" +PATH=".;$PATH:$JAVA_HOME/bin" + +添加完毕之后,按 esc 退出插入模式,并键入 wq! 保存退出文件。 + +![](images/24.png) + +到这个步骤,我们就已经配置好了全局的 java 的 path 和 classpath 环境变量。 -Max OS 开发环境搭建:(小明) \ No newline at end of file +以后就可以好好的进行 java 开发了,好嗨森!!! \ No newline at end of file diff --git a/exception.md b/exception.md new file mode 100644 index 0000000..9b49404 --- /dev/null +++ b/exception.md @@ -0,0 +1,274 @@ +# 异常处理 + +# Java 异常处理及其应用 + +Java 异常处理是使用 Java 语言进行软件开发和测试脚本开发时不容忽视的问题之一,是否进行异常处理直接关系到开发出的软件的稳定性和健壮性。本文系统的阐述了 Java 异常处理的原理和方法,并列举了一些实例,使读者对 Java 异常处理能有一个全面的认识,理解异常处理机制,能更加灵活和有效地在开发中使用它。 + +## Java 异常处理引出 +假设您要编写一个 Java 程序,该程序读入用户输入的一行文本,并在终端显示该文本。 +程序如下: + +``` +1 import java.io.*; +2 public class EchoInput { +3 public static void main(String args[]){ +4 System.out.println("Enter text to echo:"); +5 InputStreamReader isr = new InputStreamReader(System.in); +6 BufferedReader inputReader = new BufferedReader(isr); +7 String inputLine = inputReader.readLine(); +8 System.out.println("Read:" + inputLine); +9 } +10 } +``` + +分析上面的代码,在 EchoInput 类中,第 3 行声明了 main 方法;第 4 行提示用户输入文本;第 5、6 行设置 BufferedReader 对像连接到 InputStreamReader,而 InputStreamReader 又连接到标准输入流 System.in;第 7 行读入一行文本;第 8 行用标准输出流 System.out 显示出该文本。 +表面看来上面的程序没有问题,但实际上,EchoInput 类完全可能出现问题。要在调用第 7 行的 readLine 方法时正确读取输入,这几种假设都必须成立:假定键盘有效,键盘能与计算机正常通信;假定键盘数据可从操作系统传输到 Java 虚拟机,又从 Java 虚拟机传输 inputReader。 +大多数情况下上述假设都成立,但不尽然。为此,Java 采用异常方法,以应对可能出现的错误,并采取步骤进行更正。在本例中,若试图编译以上代码,将看到以下信息: + +``` +Exception in thread "main" java.lang.Error: Unresolved compilation problem: + Unhandled exception type IOException + at EchoInput.main(EchoInput.java:7) +``` + +从中可以看到,第 7 行调用 readLine 方法可能出错:若果真如此,则产生 IOException 来记录故障。编译器错误是在告诉您,需要更改代码来解决这个潜在的问题。在 JDK API 文档中,可以看到同样的信息。我们可以看到 readLine 方法,如图 1 所示。 +**图 1. BufferedReader 类的 readLine 方法的 JDK API 文档** + +![](images/34.jpg) + +由图 1 可知,readLine 方法有时产生 IOException。如何处理潜在的故障?编译器需要“捕获”或“声明”IOException。 +“捕获 (catch)”指当 readLine 方法产生错误时截获该错误,并处理和记录该问题。而“声明 (declare)”指错误可能引发 IOException,并通知调用该方法的任何代码:可能产生异常。 +若要捕获异常,必须添加一个特殊的“处理代码块”,来接收和处理 IOException。于是程序改为如下: + +``` +1 import java.io.*; +2 public class EchoInputHandle { +3 public static void main(String args[]){ +4 System.out.println("Enter text to echo:"); +5 InputStreamReader isr = new InputStreamReader(System.in); +6 BufferedReader inputReader = new BufferedReader(isr); +7 try{ +8 String inputLine = inputReader.readLine(); +9 System.out.println("Read:" + inputLine); +10 } +11 catch(IOException exc){ +12 System.out.println(“Exception encountered: ” + exc); +13 } +14 } +15 } +``` + +从上面的这个简单的例子中,我们可以看出异常处理在 Java 代码开发中不能被忽视。 +## Java 异常以及异常处理 +可将 Java 异常看作是一类消息,它传送一些系统问题、故障及未按规定执行的动作的相关信息。异常包含信息,以将信息从应用程序的一部分发送到另一部分。 +编译语言为何要处理异常?为何不在异常出现位置随时处理具体故障?因为有时候我们需要在系统中交流错误消息,以便按照统一的方式处理问题,有时是因为有若干处理问题的可能方式,但您不知道使用哪一种,此时,可将处理异常的任务委托给调用方法的代码。调用者通常更能了解问题来源的上下文,能更好的确定恢复方式。 +图 2 是一个通用消息架构。 +**图 2. 通用消息架构** + +![](images/8.gif) + +从上图可以看出,必定在运行的 Java 应用程序的一些类或对象中产生异常。出现故障时,“发送者”将产生异常对象。异常可能代表 Java 代码出现的问题,也可能是 JVM 的相应错误,或基础硬件或操作系统的错误。 +异常本身表示消息,指发送者传给接收者的数据“负荷”。首先,异常基于类的类型来传输有用信息。很多情况下,基于异常的类既能识别故障本因并能更正问题。其次,异常还带有可能有用的数据(如属性)。 +在处理异常时,消息必须有接收者;否则将无法处理产生异常的底层问题。 +在上例中,异常“产生者”是读取文本行的 BufferedReader。在故障出现时,将在 readLine 方法中构建 IOException 对象。异常“接收者”是代码本身。EchoInputHandle 应用程序的 try-catch 结构中的 catch 块是异常的接收者,它以字符串形式输出异常,将问题记录下来。 + +## Java 异常类的层次结构 +在我们从总体上了解异常后,我们应该了解如何在 Java 应用程序中使用异常,即需要了解 Java 类的层次结构。图 3 是 Java 类的层次结构图。 +**图 3. Java 类的层次结构** + +![](images/35.jpg) + +在 Java 中,所有的异常都有一个共同的祖先 Throwable(可抛出)。Throwable 指定代码中可用异常传播机制通过 Java 应用程序传输的任何问题的共性。 +Throwable 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。 +Exception(异常)是应用程序中可能的可预测、可恢复问题。一般大多数异常表示中度到轻度的问题。异常一般是在特定环境下产生的,通常出现在代码的特定方法和操作中。在 EchoInput 类中,当试图调用 readLine 方法时,可能出现 IOException 异常。 +Error(错误)表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。 +Exception 类有一个重要的子类 RuntimeException。RuntimeException 类及其子类表示“JVM 常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。 + +## Java 异常的处理 +在 Java 应用程序中,对异常的处理有两种方式:处理异常和声明异常。 +### 处理异常:try、catch 和 finally +若要捕获异常,则必须在代码中添加异常处理器块。这种 Java 结构可能包含 3 个部分, +都有 Java 关键字。下面的例子中使用了 try-catch-finally 代码结构。 + +``` +1 import java.io.*; +2 public class EchoInputTryCatchFinally { +3 public static void main(String args[]){ +4 System.out.println("Enter text to echo:"); +5 InputStreamReader isr = new InputStreamReader(System.in); +6 BufferedReader inputReader = new BufferedReader(isr); +7 try{ +8 String inputLine = inputReader.readLine(); +9 System.out.println("Read:" + inputLine); +10 } +11 catch(IOException exc){ +12 System.out.println("Exception encountered: " + exc); +13 } +14 finally{ +15 System.out.println("End. "); +16 } +17 } +18} +``` + +其中: +- try 块:将一个或者多个语句放入 try 时,则表示这些语句可能抛出异常。编译器知道可能要发生异常,于是用一个特殊结构评估块内所有语句。 +- catch 块:当问题出现时,一种选择是定义代码块来处理问题,catch 块的目的便在于此。catch 块是 try 块所产生异常的接收者。基本原理是:一旦生成异常,则 try 块的执行中止,JVM 将查找相应的 JVM。 +- finally 块:还可以定义 finally 块,无论运行 try 块代码的结果如何,该块里面的代码一定运行。在常见的所有环境中,finally 块都将运行。无论 try 块是否运行完,无论是否产生异常,也无论是否在 catch 块中得到处理,finally 块都将执行。 +try-catch-finally 规则: +1. 必须在 try 之后添加 catch 或 finally 块。try 块后可同时接 catch 和 finally 块,但至少有一个块。 +2. 必须遵循块顺序:若代码同时使用 catch 和 finally 块,则必须将 catch 块放在 try 块之后。 +3. catch 块与相应的异常类的类型相关。 +4. 一个 try 块可能有多个 catch 块。若如此,则执行第一个匹配块。 +5. 可嵌套 try-catch-finally 结构。 +6. 在 try-catch-finally 结构中,可重新抛出异常。 +7. 除了下列情况,总将执行 finally 做为结束:JVM 过早终止(调用 System.exit(int));在 finally 块中抛出一个未处理的异常;计算机断电、失火、或遭遇病毒攻击。 +### 声明异常 +若要声明异常,则必须将其添加到方法签名块的结束位置。下面是一个实例: + +``` +public void errorProneMethod(int input) throws java.io.IOException { + //Code for the method,including one or more method + //calls that may produce an IOException +} +``` + +这样,声明的异常将传给方法调用者,而且也通知了编译器:该方法的任何调用者必须遵守处理或声明规则。声明异常的规则如下: +- 必须声明方法可抛出的任何可检测异常(checked exception)。 +- 非检测性异常(unchecked exception)不是必须的,可声明,也可不声明。 +- 调用方法必须遵循任何可检测异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。 + +## Java 异常处理的分类 +Java 异常可分为可检测异常,非检测异常和自定义异常。 +### 可检测异常 +可检测异常经编译器验证,对于声明抛出异常的任何方法,编译器将强制执行处理或声明规则,例如:sqlExecption 这个异常就是一个检测异常。你连接 JDBC 时,不捕捉这个异常,编译器就通不过,不允许编译。 +### 非检测异常 +非检测异常不遵循处理或声明规则。在产生此类异常时,不一定非要采取任何适当操作,编译器不会检查是否已解决了这样一个异常。例如:一个数组为 3 个长度,当你使用下标为3时,就会产生数组下标越界异常。这个异常 JVM 不会进行检测,要靠程序员来判断。有两个主要类定义非检测异常:RuntimeException 和 Error。 +Error 子类属于非检测异常,因为无法预知它们的产生时间。若 Java 应用程序内存不足,则随时可能出现 OutOfMemoryError;起因一般不是应用程序的特殊调用,而是 JVM 自身的问题。另外,Error 一般表示应用程序无法解决的严重问题。 +RuntimeException 类也属于非检测异常,因为普通 JVM 操作引发的运行时异常随时可能发生,此类异常一般是由特定操作引发。但这些操作在 Java 应用程序中会频繁出现。因此,它们不受编译器检查与处理或声明规则的限制。 +### 自定义异常 +自定义异常是为了表示应用程序的一些错误类型,为代码可能发生的一个或多个问题提供新含义。可以显示代码多个位置之间的错误的相似性,也可以区分代码运行时可能出现的相似问题的一个或者多个错误,或给出应用程序中一组错误的特定含义。例如,对队列进行操作时,有可能出现两种情况:空队列时试图删除一个元素;满队列时试图添加一个元素。则需要自定义两个异常来处理这两种情况。 + +## Java 异常处理的原则和忌讳 +### Java 异常处理的原则 +尽可能的处理异常 +要尽可能的处理异常,如果条件确实不允许,无法在自己的代码中完成处理,就考虑声明异常。如果人为避免在代码中处理异常,仅作声明,则是一种错误和依赖的实践。 +具体问题具体解决 +异常的部分优点在于能为不同类型的问题提供不同的处理操作。有效异常处理的关键是识别特定故障场景,并开发解决此场景的特定相应行为。为了充分利用异常处理能力,需要为特定类型的问题构建特定的处理器块。 +记录可能影响应用程序运行的异常 +至少要采取一些永久的方式,记录下可能影响应用程序操作的异常。理想情况下,当然是在第一时间解决引发异常的基本问题。不过,无论采用哪种处理操作,一般总应记录下潜在的关键问题。别看这个操作很简单,但它可以帮助您用很少的时间来跟踪应用程序中复杂问题的起因。 +根据情形将异常转化为业务上下文 +若要通知一个应用程序特有的问题,有必要将应用程序转换为不同形式。若用业务特定状态表示异常,则代码更易维护。从某种意义上讲,无论何时将异常传到不同上下文(即另一技术层),都应将异常转换为对新上下文有意义的形式。 +### Java 异常处理的忌讳 +1. 一般不要忽略异常 +在异常处理块中,一项最危险的举动是“不加通告”地处理异常。如下例所示: + +``` +1 try{ +2 Class.forName("business.domain.Customer"); +3 } +4 catch (ClassNotFoundException exc){} +``` + +经常能够在代码块中看到类似的代码块。有人总喜欢在编写代码时简单快速地编写空处理器块,并“自我安慰地”宣称准备在“后期”添加恢复代码,但这个“后期”变成了“无期”。 +这种做法有什么坏处?如果异常对应用程序的其他部分确实没有任何负面影响,这未尝不可。但事实往往并非如此,异常会扰乱应用程序的状态。此时,这样的代码无异于掩耳盗铃。 +这种做法若影响较轻,则应用程序可能出现怪异行为。例如,应用程序设置的一个值不见了, 或 GUI 失效。若问题严重,则应用程序可能会出现重大问题,因为异常未记录原始故障点,难以处理,如重复的 NullPointerExceptions。 +如果采取措施,记录了捕获的异常,则不可能遇到这个问题。实际上,除非确认异常对代码其余部分绝无影响,至少也要作记录。进一步讲,永远不要忽略问题;否则,风险很大,在后期会引发难以预料的后果。 +2. 不要使用覆盖式异常处理块 +另一个危险的处理是覆盖式处理器(blanket handler)。该代码的基本结构如下: + +``` +1 try{ +2 // … +3 } +4 catch(Exception e){ +5 // … +6 } +``` + +使用覆盖式异常处理块有两个前提之一: +1. 代码中只有一类问题。 +这可能正确,但即便如此,也不应使用覆盖式异常处理,捕获更具体的异常形式有利物弊。 +2. 单个恢复操作始终适用。 +这几乎绝对错误。几乎没有哪个方法能放之四海而皆准,能应对出现的任何问题。 +分析下这样编写代码将发生的情况。只要方法不断抛出预期的异常集,则一切正常。但是,如果抛出了未预料到的异常,则无法看到要采取的操作。当覆盖式处理器对新异常类执行千篇一律的任务时,只能间接看到异常的处理结果。如果代码没有打印或记录语句,则根本看不到结果。 +更糟糕的是,当代码发生变化时,覆盖式处理器将继续作用于所有新异常类型,并以相同方式处理所有类型。 +3. 一般不要把特定的异常转化为更通用的异常 +将特定的异常转换为更通用异常时一种错误做法。一般而言,这将取消异常起初抛出时产生的上下文,在将异常传到系统的其他位置时,将更难处理。见下例: + +``` +1 try{ +2 // Error-prone code +3 } +4 catch(IOException e){ +5 String msg = "If you didn ’ t have a problem before,you do now!"; +6 throw new Exception(msg); +7 } +``` + +因为没有原始异常的信息,所以处理器块无法确定问题的起因,也不知道如何更正问题。 +4. 不要处理能够避免的异常 +对于有些异常类型,实际上根本不必处理。通常运行时异常属于此类范畴。在处理空指针或者数据索引等问题时,不必求助于异常处理。 + +## Java 异常处理的应用实例 +在定义银行类时,若取钱数大于余额时需要做异常处理。 +定义一个异常类 insufficientFundsException。取钱(withdrawal)方法中可能产生异常,条件是余额小于取额。 +处理异常在调用 withdrawal 的时候,因此 withdrawal 方法要声明抛出异常,由上一级方法调用。 +异常类: + +``` +class InsufficientFundsExceptionextends Exception{ + private Bank excepbank; // 银行对象 + private double excepAmount; // 要取的钱 + InsufficientFundsException(Bank ba, double dAmount) + { excepbank=ba; + excepAmount=dAmount; + } + public String excepMessage(){ + String str="The balance is"+excepbank.balance + + "\n"+"The withdrawal was"+excepAmount; + return str; + } +}// 异常类 +``` + +银行类: + +``` +class Bank{ + double balance;// 存款数 + Bank(double balance){this.balance=balance;} + public void deposite(double dAmount){ + if(dAmount>0.0) balance+=dAmount; + } + public void withdrawal(double dAmount) + throws InsufficientFundsException{ + if (balance li = new ArrayList(); +li.put(new Integer(3)); +Integer i = li.get(0); +``` + +在简单的程序中使用一次泛型变量不会降低罗嗦程度。但是对于多次使用泛型变量的大型程序来说,则可以累积起来降低罗嗦程度。 + +潜在的性能收益。 泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。 + +由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。 + +泛型用法的例子 + +泛型的许多最佳例子都来自集合框架,因为泛型让您在保存在集合中的元素上指定类型约束。考虑这个使用 Map 类的例子,其中涉及一定程度的优化,即 Map.get() 返回的结果将确实是一个 String: + +``` +Map m = new HashMap(); +m.put("key", "blarg"); +String s = (String) m.get("key"); +``` + +如果有人已经在映射中放置了不是 String 的其他东西,上面的代码将会抛出 ClassCastException。泛型允许您表达这样的类型约束,即 m 是一个将 String 键映射到 String 值的 Map。这可以消除代码中的强制类型转换,同时获得一个附加的类型检查层,这个检查层可以防止有人将错误类型的键或值保存在集合中。 + +下面的代码示例展示了 JDK 5.0 中集合框架中的 Map 接口的定义的一部分: + +``` +public interface Map { +public void put(K key, V value); +public V get(K key); +} +``` + +注意该接口的两个附加物: + +类型参数 K 和 V 在类级别的规格说明,表示在声明一个 Map 类型的变量时指定的类型的占位符。 + +在 get()、put() 和其他方法的方法签名中使用的 K 和 V。 + +为了赢得使用泛型的好处,必须在定义或实例化 Map 类型的变量时为 K 和 V 提供具体的值。以一种相对直观的方式做这件事: + +``` +Map m = new HashMap(); +m.put("key", "blarg"); +String s = m.get("key"); +``` + +当使用 Map 的泛型化版本时,您不再需要将 Map.get() 的结果强制类型转换为 String,因为编译器知道 get() 将返回一个 String。 + +在使用泛型的版本中并没有减少键盘录入;实际上,比使用强制类型转换的版本需要做更多键入。使用泛型只是带来了附加的类型安全。因为编译器知道关于您将放进 Map 中的键和值的类型的更多信息,所以类型检查从执行时挪到了编译时,这会提高可靠性并加快开发速度。 + + +向后兼容 + +在 Java 语言中引入泛型的一个重要目标就是维护向后兼容。尽管 JDK 5.0 的标准类库中的许多类,比如集合框架,都已经泛型化了,但是使用集合类(比如 HashMap 和 ArrayList)的现有代码将继续不加修改地在 JDK 5.0 中工作。当然,没有利用泛型的现有代码将不会赢得泛型的类型安全好处。 + + + + +二 泛型基础 + +类型参数 + +在定义泛型类或声明泛型类的变量时,使用尖括号来指定形式类型参数。形式类型参数与实际类型参数之间的关系类似于形式方法参数与实际方法参数之间的关系,只是类型参数表示类型,而不是表示值。 + +泛型类中的类型参数几乎可以用于任何可以使用类名的地方。例如,下面是 java.util.Map 接口的定义的摘录: + +``` +public interface Map { +public void put(K key, V value); +public V get(K key); +} +``` + +Map 接口是由两个类型参数化的,这两个类型是键类型 K 和值类型 V。(不使用泛型)将会接受或返回 Object 的方法现在在它们的方法签名中使用 K 或 V,指示附加的类型约束位于 Map 的规格说明之下。 + +当声明或者实例化一个泛型的对象时,必须指定类型参数的值: + +``` +Map map = new HashMap(); +``` + +注意,在本例中,必须指定两次类型参数。一次是在声明变量 map 的类型时,另一次是在选择 HashMap 类的参数化以便可以实例化正确类型的一个实例时。 + +编译器在遇到一个 Map 类型的变量时,知道 K 和 V 现在被绑定为 String,因此它知道在这样的变量上调用 Map.get() 将会得到 String 类型。 + +除了异常类型、枚举或匿名内部类以外,任何类都可以具有类型参数。 + + +命名类型参数 + +推荐的命名约定是使用大写的单个字母名称作为类型参数。这与 C++ 约定有所不同(参阅 附录 A:与 C++ 模板的比较),并反映了大多数泛型类将具有少量类型参数的假定。对于常见的泛型模式,推荐的名称是: + +K —— 键,比如映射的键。 +V —— 值,比如 List 和 Set 的内容,或者 Map 中的值。 +E —— 异常类。 +T —— 泛型。 + + +泛型不是协变的 + +关于泛型的混淆,一个常见的来源就是假设它们像数组一样是协变的。其实它们不是协变的。List 不是 List 的父类型。 + +如果 A 扩展 B,那么 A 的数组也是 B 的数组,并且完全可以在需要 B[] 的地方使用 A[]: + +``` +Integer[] intArray = new Integer[10]; +Number[] numberArray = intArray; +``` + +上面的代码是有效的,因为一个 Integer 是 一个 Number,因而一个 Integer 数组是 一个 Number 数组。但是对于泛型来说则不然。下面的代码是无效的: + +``` +List intList = new ArrayList(); +List numberList = intList; // invalid +``` + +最初,大多数 Java 程序员觉得这缺少协变很烦人,或者甚至是“坏的(broken)”,但是之所以这样有一个很好的原因。如果可以将 List 赋给 List,下面的代码就会违背泛型应该提供的类型安全: + +``` +List intList = new ArrayList(); +List numberList = intList; // invalid +numberList.add(new Float(3.1415)); +``` + +因为 intList 和 numberList 都是有别名的,如果允许的话,上面的代码就会让您将不是 Integers 的东西放进 intList 中。但是,正如下一屏将会看到的,您有一个更加灵活的方式来定义泛型。 + +类型通配符 + +假设您具有该方法: + +``` +void printList(List l) { +for (Object o : l) + System.out.println(o); +} +``` + +上面的代码在 JDK 5.0 上编译通过,但是如果试图用 List 调用它,则会得到警告。出现警告是因为,您将泛型(List)传递给一个只承诺将它当作 List(所谓的原始类型)的方法,这将破坏使用泛型的类型安全。 + +如果试图编写像下面这样的方法,那么将会怎么样? + +``` +void printList(List l) { +for (Object o : l) + System.out.println(o); +} +``` + +它仍然不会通过编译,因为一个 List 不是 一个 List(正如前一屏 泛型不是协变的 中所学的)。这才真正烦人 —— 现在您的泛型版本还没有普通的非泛型版本有用! + +解决方案是使用类型通配符: + +``` +void printList(List l) { +for (Object o : l) + System.out.println(o); +} +``` + +上面代码中的问号是一个类型通配符。它读作“问号”。List 是任何泛型 List 的父类型,所以您完全可以将 List、List 或 List>> 传递给 printList()。 + +类型通配符的作用 + +前一屏 类型通配符 中引入了类型通配符,这让您可以声明 List 类型的变量。您可以对这样的 List 做什么呢?非常方便,可以从中检索元素,但是不能添加元素。原因不是编译器知道哪些方法修改列表哪些方法不修改列表,而是(大多数)变化的方法比不变化的方法需要更多的类型信息。下面的代码则工作得很好: + +``` +List li = new ArrayList(); +li.add(new Integer(42)); +List lu = li; +System.out.println(lu.get(0)); +``` + +为什么该代码能工作呢?对于 lu,编译器一点都不知道 List 的类型参数的值。但是编译器比较聪明,它可以做一些类型推理。在本例中,它推断未知的类型参数必须扩展 Object。(这个特定的推理没有太大的跳跃,但是编译器可以作出一些非常令人佩服的类型推理,后面就会看到(在 底层细节 一节中)。所以它让您调用 List.get() 并推断返回类型为 Object。 + +另一方面,下面的代码不能工作: + +``` +List li = new ArrayList(); +li.add(new Integer(42)); +List lu = li; +lu.add(new Integer(43)); // error +``` + +在本例中,对于 lu,编译器不能对 List 的类型参数作出足够严密的推理,以确定将 Integer 传递给 List.add() 是类型安全的。所以编译器将不允许您这么做。 + +以免您仍然认为编译器知道哪些方法更改列表的内容哪些不更改列表内容,请注意下面的代码将能工作,因为它不依赖于编译器必须知道关于 lu 的类型参数的任何信息: + +``` +List li = new ArrayList(); +li.add(new Integer(42)); +List lu = li; +lu.clear(); +``` + +泛型方法 + +(在 类型参数 一节中)您已经看到,通过在类的定义中添加一个形式类型参数列表,可以将类泛型化。方法也可以被泛型化,不管它们定义在其中的类是不是泛型化的。 + +泛型类在多个方法签名间实施类型约束。在 List 中,类型参数 V 出现在 get()、add()、contains() 等方法的签名中。当创建一个 Map 类型的变量时,您就在方法之间宣称一个类型约束。您传递给 add() 的值将与 get() 返回的值的类型相同。 + +类似地,之所以声明泛型方法,一般是因为您想要在该方法的多个参数之间宣称一个类型约束。例如,下面代码中的 ifThenElse() 方法,根据它的第一个参数的布尔值,它将返回第二个或第三个参数: + +``` +public T ifThenElse(boolean b, T first, T second) { +return b ? first : second; +} +``` + +注意,您可以调用 ifThenElse(),而不用显式地告诉编译器,您想要 T 的什么值。编译器不必显式地被告知 T 将具有什么值;它只知道这些值都必须相同。编译器允许您调用下面的代码,因为编译器可以使用类型推理来推断出,替代 T 的 String 满足所有的类型约束: + +``` +String s = ifThenElse(b, "a", "b"); +``` + +类似地,您可以调用: + +``` +Integer i = ifThenElse(b, new Integer(1), new Integer(2)); +``` + +但是,编译器不允许下面的代码,因为没有类型会满足所需的类型约束: + +``` +String s = ifThenElse(b, "pi", new Float(3.14)); +``` + +为什么您选择使用泛型方法,而不是将类型 T 添加到类定义呢?(至少)有两种情况应该这样做: + +当泛型方法是静态的时,这种情况下不能使用类类型参数。 + +当 T 上的类型约束对于方法真正是局部的时,这意味着没有在相同类的另一个 方法签名中使用相同 类型 T 的约束。通过使得泛型方法的类型参数对于方法是局部的,可以简化封闭类型的签名。 + +有限制类型 + +在前一屏 泛型方法 的例子中,类型参数 V 是无约束的或无限制的 类型。有时在还没有完全指定类型参数时,需要对类型参数指定附加的约束。 + +考虑例子 Matrix 类,它使用类型参数 V,该参数由 Number 类来限制: + +``` +public class Matrix { ... } +``` + +编译器允许您创建 Matrix 或 Matrix 类型的变量,但是如果您试图定义 Matrix 类型的变量,则会出现错误。类型参数 V 被判断为由 Number 限制 。在没有类型限制时,假设类型参数由 Object 限制。这就是为什么前一屏 泛型方法 中的例子,允许 List.get() 在 List 上调用时返回 Object,即使编译器不知道类型参数 V 的类型。 + +三 一个简单的泛型类 + +编写基本的容器类 + +此时,您可以开始编写简单的泛型类了。到目前为止,泛型类最常见的用例是容器类(比如集合框架)或者值持有者类(比如 WeakReference 或 ThreadLocal)。我们来编写一个类,它类似于 List,充当一个容器。其中,我们使用泛型来表示这样一个约束,即 Lhist 的所有元素将具有相同类型。为了实现起来简单,Lhist 使用一个固定大小的数组来保存值,并且不接受 null 值。 + +Lhist 类将具有一个类型参数 V(该参数是 Lhist 中的值的类型),并将具有以下方法: + +``` +public class Lhist { +public Lhist(int capacity) { ... } +public int size() { ... } +public void add(V value) { ... } +public void remove(V value) { ... } +public V get(int index) { ... } +} +``` + +要实例化 Lhist,只要在声明时指定类型参数和想要的容量: + +``` +Lhist stringList = new Lhist(10); +``` + +实现构造函数 + +在实现 Lhist 类时,您将会遇到的第一个拦路石是实现构造函数。您可能会像下面这样实现它: + +``` +public class Lhist { +private V[] array; +public Lhist(int capacity) { + array = new V[capacity]; // illegal +} +} +``` + +这似乎是分配后备数组最自然的一种方式,但是不幸的是,您不能这样做。具体原因很复杂,当学习到 底层细节 一节中的“擦除”主题时,您就会明白。分配后备数组的实现方式很古怪且违反直觉。下面是构造函数的一种可能的实现(该实现使用集合类所采用的方法): + +``` +public class Lhist { +private V[] array; +public Lhist(int capacity) { + array = (V[]) new Object[capacity]; +} +} +``` + +另外,也可以使用反射来实例化数组。但是这样做需要给构造函数传递一个附加的参数 —— 一个类常量,比如 Foo.class。后面在 Class 一节中将讨论类常量。 + +实现方法 + +实现 Lhist 的方法要容易得多。下面是 Lhist 类的完整实现: + +``` +public class Lhist { + private V[] array; + private int size; + + public Lhist(int capacity) { + array = (V[]) new Object[capacity]; + } + + public void add(V value) { + if (size == array.length) + throw new IndexOutOfBoundsException(Integer.toString(size)); + else if (value == null) + throw new NullPointerException(); + array[size++] = value; + } + + public void remove(V value) { + int removalCount = 0; + for (int i=0; i 0) { + array[i-removalCount] = array[i]; + array[i] = null; + } + } + size -= removalCount; + } + + public int size() { return size; } + + public V get(int i) { + if (i >= size) + throw new IndexOutOfBoundsException(Integer.toString(i)); + return array[i]; + } +} +``` + +注意,您在将会接受或返回 V 的方法中使用了形式类型参数 V,但是您一点也不知道 V 具有什么样的方法或域,因为这些对泛型代码是不可知的。 + +使用 Lhist 类 + +使用 Lhist 类很容易。要定义一个整数 Lhist,只需要在声明和构造函数中为类型参数提供一个实际值即可: + +Lhist li = new Lhist(30); + +编译器知道,li.get() 返回的任何值都将是 Integer 类型,并且它还强制传递给 li.add() 或 li.remove() 的任何东西都是 Integer。除了实现构造函数的方式很古怪之外,您不需要做任何十分特殊的事情以使 Lhist 是一个泛型类。 + +四 Java类库中的泛型 + +集合类 + +到目前为止,Java 类库中泛型支持存在最多的地方就是集合框架。就像容器类是 C++ 语言中模板的主要动机一样(参阅 附录 A:与 C++ 模板的比较)(尽管它们随后用于很多别的用途),改善集合类的类型安全是 Java 语言中泛型的主要动机。集合类也充当如何使用泛型的模型,因为它们演示了泛型的几乎所有的标准技巧和方言。 + +所有的标准集合接口都是泛型化的 —— Collection、List、Set 和 Map。类似地,集合接口的实现都是用相同类型参数泛型化的,所以 HashMap 实现 Map 等。 + +集合类也使用泛型的许多“技巧”和方言,比如上限通配符和下限通配符。例如,在接口 Collection 中,addAll 方法是像下面这样定义的: + +``` +interface Collection { +boolean addAll(Collection); +} +``` + +该定义组合了通配符类型参数和有限制类型参数,允许您将 Collection 的内容添加到 Collection。 + +如果类库将 addAll() 定义为接受 Collection,您就不能将 Collection 的内容添加到 Collection。不是限制 addAll() 的参数是一个与您将要添加到的集合包含相同类型的集合,而有可能建立一个更合理的约束,即传递给 addAll() 的集合的元素 适合于添加到您的集合。有限制类型允许您这样做,并且使用有限制通配符使您不需要使用另一个不会用在其他任何地方的占位符名称。 + +应该可以将 addAll() 的类型参数定义为 Collection。但是,这不但没什么用,而且还会改变 Collection 接口的语义,因为泛型版本的语义将会不同于非泛型版本的语义。这阐述了泛型化一个现有的类要比定义一个新的泛型类难得多,因为您必须注意不要更改类的语义或者破坏现有的非泛型代码。 + +作为泛型化一个类(如果不小心的话)如何会更改其语义的一个更加微妙的例子,注意 Collection.removeAll() 的参数的类型是 Collection,而不是 Collection。这是因为传递混合类型的集合给 removeAll() 是可接受的,并且更加限制地定义 removeAll 将会更改方法的语义和有用性。 + +其他容器类 + +除了集合类之外,Java 类库中还有几个其他的类也充当值的容器。这些类包括 WeakReference、SoftReference 和 ThreadLocal。它们都已经在其包含的值的类型上泛型化了,所以 WeakReference 是对 T 类型的对象的弱引用,ThreadLocal 则是到 T 类型的线程局部变量的句柄。 + +泛型不止用于容器 + +泛型最常见最直观的使用是容器类,比如集合类或引用类(比如 WeakReference)。Collection 中类型参数的含义很明显 —— “一个所有值都是 V 类型的集合”。类似地,ThreadLocal 也有一个明显的解释 —— “一个其类型是 T 的线程局部变量”。但是,泛型规格说明中没有指定容积。 + +像 Comparable 或 Class 这样的类中类型参数的含义更加微妙。有时,就像 Class 中一样,类型变量主要是帮助编译器进行类型推理。有时,就像隐含的 Enum> 中一样,类型变量只是在类层次结构上加一个约束。 + +Comparable + +Comparable 接口已经泛型化了,所以实现 Comparable 的对象声明它可以与什么类型进行比较。(通常,这是对象本身的类型,但是有时也可能是父类。) + +``` +public interface Comparable { +public boolean compareTo(T other); +} +``` + +所以 Comparable 接口包含一个类型参数 T,该参数是一个实现 Comparable 的类可以与之比较的对象的类型。这意味着如果定义一个实现 Comparable 的类,比如 String,就必须不仅声明类支持比较,还要声明它可与什么比较(通常是与它本身比较): + +``` +public class String implements Comparable { ... } +``` + +现在来考虑一个二元 max() 方法的实现。您想要接受两个相同类型的参数,二者都是 Comparable,并且相互之间是 Comparable。幸运的是,如果使用泛型方法和有限制类型参数的话,这相当直观: + +``` +public static > T max(T t1, T t2) { +if (t1.compareTo(t2) > 0) + return t1; +else + return t2; +} +``` + +在本例中,您定义了一个泛型方法,在类型 T 上泛型化,您约束该类型扩展(实现) Comparable。两个参数都必须是 T 类型,这表示它们是相同类型,支持比较,并且相互可比较。容易! + +更好的是,编译器将使用类型推理来确定当调用 max() 时 T 的值表示什么意思。所以根本不用指定 T,下面的调用就能工作: + +``` +String s = max("moo", "bark"); +``` + +编译器将计算出 T 的预定值是 String,因此它将进行编译和类型检查。但是如果您试图用不实现 Comparable 的 类 X 的参数调用 max(),那么编译器将不允许这样做。 + +``` +Class +``` + +类 Class 已经泛型化了,但是很多人一开始都感觉其泛型化的方式很混乱。Class 中类型参数 T 的含义是什么?事实证明它是所引用的类接口。怎么会是这样的呢?那是一个循环推理?如果不是的话,为什么这样定义它? + +在以前的 JDK 中,Class.newInstance() 方法的定义返回 Object,您很可能要将该返回类型强制转换为另一种类型: + +``` +class Class { +Object newInstance(); +} +``` + +但是使用泛型,您定义 Class.newInstance() 方法具有一个更加特定的返回类型: + +``` +class Class { +T newInstance(); +} +``` + +如何创建一个 Class 类型的实例?就像使用非泛型代码一样,有两种方式:调用方法 Class.forName() 或者使用类常量 X.class。Class.forName() 被定义为返回 Class。另一方面,类常量 X.class 被定义为具有类型 Class,所以 String.class 是 Class 类型的。 + +让 Foo.class 是 Class 类型的有什么好处?大的好处是,通过类型推理的魔力,可以提高使用反射的代码的类型安全。另外,还不需要将 Foo.class.newInstance() 强制类型转换为 Foo。 + +考虑一个方法,它从数据库检索一组对象,并返回 JavaBeans 对象的一个集合。您通过反射来实例化和初始化创建的对象,但是这并不意味着类型安全必须完全被抛至脑后。考虑下面这个方法: + +``` +public static List getRecords(Class c, Selector s) { +// Use Selector to select rows +List list = new ArrayList(); +for (/* iterate over results */) { + T row = c.newInstance(); + // use reflection to set fields from result + list.add(row); +} +return list; +} +``` + +可以像下面这样简单地调用该方法: + +``` +List l = getRecords(FooRecord.class, fooSelector); +``` + +编译器将会根据 FooRecord.class 是 Class 类型的这一事实,推断 getRecords() 的返回类型。您使用类常量来构造新的实例并提供编译器在类型检查中要用到的类型信息。 + +用 Class 替换 T[] + +Collection 接口包含一个方法,用于将集合的内容复制到一个调用者指定类型的数组中: + +``` +public Object[] toArray(Object[] prototypeArray) { ... } +``` + +toArray(Object[]) 的语义是,如果传递的数组足够大,就会使用它来保存结果,否则,就会使用反射分配一个相同类型的新数组。一般来说,单独传递一个数组作为参数来提供想要的返回类型是一个小技巧,但是在引入泛型之前,这是与方法交流类型信息最方便的方式。 + +有了泛型,就可以用一种更加直观的方式来做这件事。不像上面这样定义 toArray(),泛型 toArray() 可能看起来像下面这样: + +``` +public T[] toArray(Class returnType) +``` + +调用这样一个 toArray() 方法很简单: + +``` +FooBar[] fba = something.toArray(FooBar.class); +``` + +Collection 接口还没有改变为使用该技术,因为这会破坏许多现有的集合实现。但是如果使用泛型从新构建 Collection,则当然会使用该方言来指定它想要返回值是哪种类型。 + +Enum + +JDK 5.0 中 Java 语言另一个增加的特性是枚举。当您使用 enum 关键字声明一个枚举时,编译器就会在内部为您生成一个类,用于扩展 Enum 并为枚举的每个值声明静态实例。所以如果您说: + +``` +public enum Suit {HEART, DIAMOND, CLUB, SPADE}; +``` + +编译器就会在内部生成一个叫做 Suit 的类,该类扩展 java.lang.Enum 并具有叫做 HEART、DIAMOND、CLUB 和 SPADE 的常量(public static final)成员,每个成员都是 Suit 类。 + +与 Class 一样,Enum 也是一个泛型类。但是与 Class 不同,它的签名稍微更复杂一些: + +``` +class Enum> { . . . } +``` + +这究竟是什么意思?这难道不会导致无限递归? + +我们逐步来分析。类型参数 E 用于 Enum 的各种方法中,比如 compareTo() 或 getDeclaringClass()。为了这些方法的类型安全,Enum 类必须在枚举的类上泛型化。 + +所以 extends Enum 部分如何理解?该部分又具有两个部分。第一部分指出,作为 Enum 的类型参数的类本身必须是 Enum 的子类型,所以您不能声明一个类 X 扩展 Enum。第二部分指出,任何扩展 Enum 的类必须传递它本身 作为类型参数。您不能声明 X 扩展 Enum,即使 Y 扩展 Enum。 + +总之,Enum 是一个参数化的类型,只可以为它的子类型实例化,并且这些子类型然后将根据子类型来继承方法。幸运的是,在 Enum 情况下,编译器为您做这些工作,一切都很好。 + +与非泛型代码相互操作 + +数百万行现有代码使用已经泛型化的 Java 类库中的类,比如集合框架、Class 和 ThreadLocal。JDK 5.0 中的改进不要破坏所有这些代码是很重要的,所以编译器允许您在不指定其类型参数的情况下使用泛型类。 + +当然,以“旧方式”做事没有新方式安全,因为忽略了编译器准备提供的类型安全。如果您试图将 List 传递给一个接受 List 的方法,它将能够工作,但是编译器将会发出一个可能丧失类型安全的警告,即所谓的“unchecked conversion(不检查转换)”警告。 + +没有类型参数的泛型,比如声明为 List 类型而不是 List 类型的变量,叫做原始类型。原始类型与参数化类型的任何实例化是赋值兼容的,但是这样的赋值会生成 unchecked-conversion 警告。 + +为了消除一些 unchecked-conversion 警告,假设您不准备泛型化所有的代码,您可以使用通配符类型参数。使用 List 而不使用 List。List 是原始类型;List 是具有未知类型参数的泛型。编译器将以不同的方式对待它们,并很可能发出更少的警告。 + +无论在哪种情况下,编译器在生成字节码时都会生成强制类型转换,所以生成的字节码在每种情况下都不会比没有泛型时更不安全。如果您设法通过使用原始类型或类文件来破坏类型安全,就会得到与不使用泛型时得到的相同的 ClassCastException 或 ArrayStoreException。 + +已检查集合 + +作为从原始集合类型迁移到泛型集合类型的帮助,集合框架添加了一些新的集合包装器,以便为一些类型安全 bug 提供早期警告。就像 Collections.unmodifiableSet() 工厂方法用一个不允许任何修改的 Set 包装一个现有 Set 一样,Collections.checkedSet()(以及 checkedList() 和 checkedMap())工厂方法创建一个包装器(或者视图)类,以防止您将错误类型的变量放在集合中。 + +checkedXxx() 方法都接受一个类常量作为参数,所以它们可以(在运行时)检查这些修改是允许的。典型的实现可能像下面这样: + +``` +public class Collections { +public static Collection checkedCollection(Collection c, Class type ) { + return new CheckedCollection(c, type); +} + +private static class CheckedCollection implements Collection { + private final Collection c; + private final Class type; + + CheckedCollection(Collection c, Class type) { + this.c = c; + this.type = type; + } + + public boolean add(E o) { + if (!type.isInstance(o)) + throw new ClassCastException(); + else + return c.add(o); + } +} +} +``` \ No newline at end of file diff --git a/hibernate.md b/hibernate.md new file mode 100644 index 0000000..8130e77 --- /dev/null +++ b/hibernate.md @@ -0,0 +1,325 @@ +# Hibernate + +# Hibernate 框架简述 + +## Hibernate 的核心组件 +在基于 MVC 设计模式的 JAVA WEB 应用中,Hibernate 可以作为模型层/数据访问层。它通过配置文件(hibernate.properties 或 hibernate.cfg.xml)和映射文件(***.hbm.xml)把 JAVA 对象或 PO(Persistent Object,持久化对象)映射到数据库中的数据库,然后通过操作 PO,对数据表中的数据进行增,删,改,查等操作。 +除配置文件,映射文件和持久化类外,Hibernate 的核心组件包括以下几部分: +a)Configuration 类:用来读取 Hibernate 配置文件,并生成 SessionFactory 对象。 +b)SessionFactory 接口:产生 Session 实例工厂。 +c)Session 接口:用来操作 PO。它有 get(),load(),save(),update()和 delete()等方法用来对 PO 进行加载,保存,更新及删除等操作。它是 Hibernate 的核心接口。 +d)Query 接口:用来对 PO 进行查询操。它可以从 Session 的 createQuery()方法生成。 +e)Transaction 接口:用来管理 Hibernate 事务,它主要方法有 commit()和 rollback(),可以从 Session 的 beginTrancation()方法生成。 + +## Persistent Object +持久化对象可以是普通的 Javabeans,惟一特殊的是它们与(仅一个)Session 相关联。 JavaBeans 在 Hibernate 中存在三种状态: +1. 临时状态(transient):当一个 JavaBean 对象在内存中孤立存在,不与数据库中的数据有任何关联关系时,那么这个 JavaBeans 对象就称为临时对象(Transient Object)。 +2. 持久化状态(persistent):当一个 JavaBean 对象与一个 Session 相关联时,就变成持久化对象(Persistent Object) +3. 脱管状态(detached):在这个 Session 被关闭的同时,这个对象也会脱离持久化状态,就变成脱管状态(Detached Object),可以被应用程序的任何层自由使用,例如可以做与表示层打交道的数据舆对象(Data Transfer Object)。 + +## Hibernate 的运行过程 +Hibernate 的运行过程如下: +A:应用程序先调用 Configration 类,该类读取 Hibernate 的配置文件及映射文件中的信息,并用这些信息生成一个 SessionFactpry 对象。 +B:然后从 SessionFactory 对象生成一个 Session 对象,并用 Session 对象生成 Transaction 对象;可通过 Session 对象的 get(),load(),save(),update(),delete()和 saveOrUpdate()等方法对 PO 进行加载,保存,更新,删除等操作;在查询的情况下,可通过 Session 对象生成一个 Query 对象,然后利用 Query 对象执行查询操作;如果没有异常,Transaction 对象将提交这些操作结果到数据库中。 + +Hibernate 的运行过程如下图: + +![](images/40.jpg) + +Hibernate 简单示例: +数据: + +``` +create table T_register +( + id int primary key, + userName varchar(30), + userPwd varchar(30), + sex varchar(10), + age int +) +``` + +视图层:注册页面 register.jsp + +``` +<%@ page language="java" contentType="text/html; charset=GBK" + pageEncoding="GBK"%> + + + + +Insert title here + + + +
+ 用户名:
+ 密 码:
+ 性 别:
+ 年 龄:
+ +
+ + +``` + +设计持久化类 TRegister.java + +``` +package hibernate.PO; +/** + * 持久化类 + */ + +public class TRegister implements java.io.Serializable { + + + // Fields + + private Integer id; + private String userName; + private String userPwd; + private String sex; + private Integer age; + + + // Constructors + + /** default constructor */ + public TRegister() { + } + + /** minimal constructor */ + public TRegister(Integer id) { + this.id = id; + } + + /** full constructor */ + public TRegister(Integer id, String userName, String userPwd, String sex, Integer age) { + this.id = id; + this.userName = userName; + this.userPwd = userPwd; + this.sex = sex; + this.age = age; + } + + + // Property accessors + + public Integer getId() { + return this.id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getUserName() { + return this.userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getUserPwd() { + return this.userPwd; + } + + public void setUserPwd(String userPwd) { + this.userPwd = userPwd; + } + + public String getSex() { + return this.sex; + } + + public void setSex(String sex) { + this.sex = sex; + } + + public Integer getAge() { + return this.age; + } + + public void setAge(Integer age) { + this.age = age; + } +} +``` + +设计 Hibernate 配置文件 hibernate.cfg.xml + +``` + + + + + + + + root + + jdbc:mysql://localhost:3306/study + + + org.hibernate.dialect.MySQLDialect + + MySQL5.0 + root + + org.gjt.mm.mysql.Driver + + true + + + + + +``` + +设计映射文件 TRegister.hbm.xml + +``` +TRegister.hbm.xml 设计 hibernate 基础类 HibernateUtil.java +package hibernate; +/** + * hibernate 基础类 + * @author fengyan + * date 2007-01-09 02:32 + */ +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.cfg.Configuration; + +public class HibernateUtil { + + private static final SessionFactory sessionFactory; + + static + { + try + { + Configuration config = new Configuration().configure("/hibernate/hibernate.cfg.xml"); + sessionFactory = config.buildSessionFactory(); + } + catch(Throwable e) + { + throw new ExceptionInInitializerError(e); + } + } + + public static final ThreadLocal session = new ThreadLocal(); + + public static Session currentSession() throws HibernateException + { + Session s = (Session)session.get(); + //Open a new Session,if this Thread has none yet + if(s == null || !s.isOpen()) + { + s = sessionFactory.openSession(); + session.set(s); + } + return s; + } + + public static void closeSession() throws HibernateException + { + Session s = (Session)session.get(); + session.set(null); + if(s != null) + s.close(); + } + +} +``` + +设计控制类 + +``` +package hibernate.servlet; +/** + * @author fengyan + * date 2007-01-09 02:44 + * 设计Hibernate控制类 + */ +import hibernate.HibernateUtil; +import hibernate.PO.TRegister; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.Transaction; + +public class RegisterServlet extends HttpServlet { + + private static final String CONTENT_TYPE = "text/html;charset=GBK"; + public void init() throws ServletException { + // Put your code here + } + public void destroy() { + super.destroy(); // Just puts "destroy" string in log + // Put your code here + } + + + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + response.setContentType(CONTENT_TYPE); + request.setCharacterEncoding("GBK"); + + PrintWriter out = response.getWriter(); + + String userName = request.getParameter("userName"); + String userPwd = request.getParameter("userPwd"); + String sex = request.getParameter("sex"); + int age = Integer.parseInt(request.getParameter("age")); + + TRegister rg = new TRegister(); + rg.setAge(age); + rg.setSex(sex); + rg.setUserName(userName); + rg.setUserPwd(userPwd); + + Session session = HibernateUtil.currentSession();//生成Session实例 + Transaction tx = session.beginTransaction(); + + try + { + session.save(rg); //保存持久类对象 + tx.commit(); //提交到数据库 + session.close(); + response.sendRedirect("registerOK.jsp"); + } + catch(HibernateException e) + { + e.printStackTrace(); + tx.rollback(); + } + + } + + public void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + doGet(request,response); + + } + + + +} +``` + +编译----->打包----->运行----->OK! \ No newline at end of file diff --git a/images/1.gif b/images/1.gif new file mode 100644 index 0000000..ba15fa0 Binary files /dev/null and b/images/1.gif differ diff --git a/images/1.jpg b/images/1.jpg new file mode 100644 index 0000000..a1a903f Binary files /dev/null and b/images/1.jpg differ diff --git a/images/10.gif b/images/10.gif new file mode 100644 index 0000000..2020165 Binary files /dev/null and b/images/10.gif differ diff --git a/images/10.jpg b/images/10.jpg new file mode 100644 index 0000000..440ba89 Binary files /dev/null and b/images/10.jpg differ diff --git a/images/10.png b/images/10.png new file mode 100644 index 0000000..2f405d8 Binary files /dev/null and b/images/10.png differ diff --git a/images/11.gif b/images/11.gif new file mode 100644 index 0000000..e24d53e Binary files /dev/null and b/images/11.gif differ diff --git a/images/11.jpg b/images/11.jpg new file mode 100644 index 0000000..4965b96 Binary files /dev/null and b/images/11.jpg differ diff --git a/images/11.png b/images/11.png new file mode 100644 index 0000000..27333f6 Binary files /dev/null and b/images/11.png differ diff --git a/images/12.jpg b/images/12.jpg new file mode 100644 index 0000000..592f8a2 Binary files /dev/null and b/images/12.jpg differ diff --git a/images/12.png b/images/12.png new file mode 100644 index 0000000..b203279 Binary files /dev/null and b/images/12.png differ diff --git a/images/13.jpg b/images/13.jpg new file mode 100644 index 0000000..2d6396f Binary files /dev/null and b/images/13.jpg differ diff --git a/images/13.png b/images/13.png new file mode 100644 index 0000000..377989d Binary files /dev/null and b/images/13.png differ diff --git a/images/14.jpg b/images/14.jpg new file mode 100644 index 0000000..dd474ec Binary files /dev/null and b/images/14.jpg differ diff --git a/images/14.png b/images/14.png new file mode 100644 index 0000000..fcf8f8d Binary files /dev/null and b/images/14.png differ diff --git a/images/15.jpg b/images/15.jpg new file mode 100644 index 0000000..b34ce66 Binary files /dev/null and b/images/15.jpg differ diff --git a/images/15.png b/images/15.png new file mode 100644 index 0000000..9cfd4fd Binary files /dev/null and b/images/15.png differ diff --git a/images/16.jpg b/images/16.jpg new file mode 100644 index 0000000..c9d5a04 Binary files /dev/null and b/images/16.jpg differ diff --git a/images/16.png b/images/16.png new file mode 100644 index 0000000..db34857 Binary files /dev/null and b/images/16.png differ diff --git a/images/17.jpg b/images/17.jpg new file mode 100644 index 0000000..b812284 Binary files /dev/null and b/images/17.jpg differ diff --git a/images/17.png b/images/17.png new file mode 100644 index 0000000..f602332 Binary files /dev/null and b/images/17.png differ diff --git a/images/18.jpg b/images/18.jpg new file mode 100644 index 0000000..7eeb0f8 Binary files /dev/null and b/images/18.jpg differ diff --git a/images/18.png b/images/18.png new file mode 100644 index 0000000..e6f4a1e Binary files /dev/null and b/images/18.png differ diff --git a/images/19.jpg b/images/19.jpg new file mode 100644 index 0000000..3f80a1a Binary files /dev/null and b/images/19.jpg differ diff --git a/images/19.png b/images/19.png new file mode 100644 index 0000000..b46bbb3 Binary files /dev/null and b/images/19.png differ diff --git a/images/2.gif b/images/2.gif new file mode 100644 index 0000000..5b3b9de Binary files /dev/null and b/images/2.gif differ diff --git a/images/2.jpg b/images/2.jpg new file mode 100644 index 0000000..43bd3a5 Binary files /dev/null and b/images/2.jpg differ diff --git a/images/20.jpg b/images/20.jpg new file mode 100644 index 0000000..1c7f923 Binary files /dev/null and b/images/20.jpg differ diff --git a/images/20.png b/images/20.png new file mode 100644 index 0000000..88d9aa8 Binary files /dev/null and b/images/20.png differ diff --git a/images/21.jpg b/images/21.jpg new file mode 100644 index 0000000..949288b Binary files /dev/null and b/images/21.jpg differ diff --git a/images/21.png b/images/21.png new file mode 100644 index 0000000..576ad20 Binary files /dev/null and b/images/21.png differ diff --git a/images/22.jpg b/images/22.jpg new file mode 100644 index 0000000..735b509 Binary files /dev/null and b/images/22.jpg differ diff --git a/images/22.png b/images/22.png new file mode 100644 index 0000000..e0cefdb Binary files /dev/null and b/images/22.png differ diff --git a/images/23.jpg b/images/23.jpg new file mode 100644 index 0000000..9b32b99 Binary files /dev/null and b/images/23.jpg differ diff --git a/images/23.png b/images/23.png new file mode 100644 index 0000000..e35c8d0 Binary files /dev/null and b/images/23.png differ diff --git a/images/24.jpg b/images/24.jpg new file mode 100644 index 0000000..2e5c5a6 Binary files /dev/null and b/images/24.jpg differ diff --git a/images/24.png b/images/24.png new file mode 100644 index 0000000..583fe89 Binary files /dev/null and b/images/24.png differ diff --git a/images/25.jpg b/images/25.jpg new file mode 100644 index 0000000..e37e5b4 Binary files /dev/null and b/images/25.jpg differ diff --git a/images/25.png b/images/25.png new file mode 100644 index 0000000..08553b9 Binary files /dev/null and b/images/25.png differ diff --git a/images/26.jpg b/images/26.jpg new file mode 100644 index 0000000..ed30ae7 Binary files /dev/null and b/images/26.jpg differ diff --git a/images/26.png b/images/26.png new file mode 100644 index 0000000..aa5c5ef Binary files /dev/null and b/images/26.png differ diff --git a/images/27.jpg b/images/27.jpg new file mode 100644 index 0000000..15e2f44 Binary files /dev/null and b/images/27.jpg differ diff --git a/images/27.png b/images/27.png new file mode 100644 index 0000000..87426b3 Binary files /dev/null and b/images/27.png differ diff --git a/images/28.jpg b/images/28.jpg new file mode 100644 index 0000000..46f239e Binary files /dev/null and b/images/28.jpg differ diff --git a/images/28.png b/images/28.png new file mode 100644 index 0000000..6702061 Binary files /dev/null and b/images/28.png differ diff --git a/images/29.jpg b/images/29.jpg new file mode 100644 index 0000000..800f776 Binary files /dev/null and b/images/29.jpg differ diff --git a/images/29.png b/images/29.png new file mode 100644 index 0000000..772de0d Binary files /dev/null and b/images/29.png differ diff --git a/images/3.gif b/images/3.gif new file mode 100644 index 0000000..f3c36f9 Binary files /dev/null and b/images/3.gif differ diff --git a/images/3.jpg b/images/3.jpg new file mode 100644 index 0000000..a471310 Binary files /dev/null and b/images/3.jpg differ diff --git a/images/30.jpg b/images/30.jpg new file mode 100644 index 0000000..f7a657e Binary files /dev/null and b/images/30.jpg differ diff --git a/images/30.png b/images/30.png new file mode 100644 index 0000000..c065780 Binary files /dev/null and b/images/30.png differ diff --git a/images/31.jpg b/images/31.jpg new file mode 100644 index 0000000..8bfb1c4 Binary files /dev/null and b/images/31.jpg differ diff --git a/images/31.png b/images/31.png new file mode 100644 index 0000000..9593d4d Binary files /dev/null and b/images/31.png differ diff --git a/images/32.PNG b/images/32.PNG new file mode 100644 index 0000000..87cde0d Binary files /dev/null and b/images/32.PNG differ diff --git a/images/32.jpg b/images/32.jpg new file mode 100644 index 0000000..2455e5e Binary files /dev/null and b/images/32.jpg differ diff --git a/images/33.jpg b/images/33.jpg new file mode 100644 index 0000000..e968c8a Binary files /dev/null and b/images/33.jpg differ diff --git a/images/33.png b/images/33.png new file mode 100644 index 0000000..df33198 Binary files /dev/null and b/images/33.png differ diff --git a/images/34.jpg b/images/34.jpg new file mode 100644 index 0000000..f784df3 Binary files /dev/null and b/images/34.jpg differ diff --git a/images/34.png b/images/34.png new file mode 100644 index 0000000..ab088dc Binary files /dev/null and b/images/34.png differ diff --git a/images/35.jpg b/images/35.jpg new file mode 100644 index 0000000..70ef12e Binary files /dev/null and b/images/35.jpg differ diff --git a/images/35.png b/images/35.png new file mode 100644 index 0000000..ceb512c Binary files /dev/null and b/images/35.png differ diff --git a/images/36.jpg b/images/36.jpg new file mode 100644 index 0000000..07e15b9 Binary files /dev/null and b/images/36.jpg differ diff --git a/images/36.png b/images/36.png new file mode 100644 index 0000000..9a1a9cb Binary files /dev/null and b/images/36.png differ diff --git a/images/37.jpg b/images/37.jpg new file mode 100644 index 0000000..9f7d359 Binary files /dev/null and b/images/37.jpg differ diff --git a/images/37.png b/images/37.png new file mode 100644 index 0000000..c725472 Binary files /dev/null and b/images/37.png differ diff --git a/images/38.jpg b/images/38.jpg new file mode 100644 index 0000000..4cd2b2f Binary files /dev/null and b/images/38.jpg differ diff --git a/images/38.png b/images/38.png new file mode 100644 index 0000000..8c0f5b8 Binary files /dev/null and b/images/38.png differ diff --git a/images/39.jpg b/images/39.jpg new file mode 100644 index 0000000..7717229 Binary files /dev/null and b/images/39.jpg differ diff --git a/images/39.png b/images/39.png new file mode 100644 index 0000000..d0aba43 Binary files /dev/null and b/images/39.png differ diff --git a/images/4.gif b/images/4.gif new file mode 100644 index 0000000..9895c47 Binary files /dev/null and b/images/4.gif differ diff --git a/images/4.jpg b/images/4.jpg new file mode 100644 index 0000000..b7586d1 Binary files /dev/null and b/images/4.jpg differ diff --git a/images/40.jpg b/images/40.jpg new file mode 100644 index 0000000..4cb7096 Binary files /dev/null and b/images/40.jpg differ diff --git a/images/40.png b/images/40.png new file mode 100644 index 0000000..a960797 Binary files /dev/null and b/images/40.png differ diff --git a/images/41.png b/images/41.png new file mode 100644 index 0000000..3d9e100 Binary files /dev/null and b/images/41.png differ diff --git a/images/42.png b/images/42.png new file mode 100644 index 0000000..d4d7eb6 Binary files /dev/null and b/images/42.png differ diff --git a/images/43.png b/images/43.png new file mode 100644 index 0000000..ad257ba Binary files /dev/null and b/images/43.png differ diff --git a/images/44.png b/images/44.png new file mode 100644 index 0000000..96f474d Binary files /dev/null and b/images/44.png differ diff --git a/images/45.png b/images/45.png new file mode 100644 index 0000000..531c407 Binary files /dev/null and b/images/45.png differ diff --git a/images/46.png b/images/46.png new file mode 100644 index 0000000..a7a47d8 Binary files /dev/null and b/images/46.png differ diff --git a/images/47.png b/images/47.png new file mode 100644 index 0000000..5c42be8 Binary files /dev/null and b/images/47.png differ diff --git a/images/48.png b/images/48.png new file mode 100644 index 0000000..b673452 Binary files /dev/null and b/images/48.png differ diff --git a/images/49.png b/images/49.png new file mode 100644 index 0000000..7919e54 Binary files /dev/null and b/images/49.png differ diff --git a/images/5.gif b/images/5.gif new file mode 100644 index 0000000..3ce7f3f Binary files /dev/null and b/images/5.gif differ diff --git a/images/5.jpg b/images/5.jpg new file mode 100644 index 0000000..04e6a5f Binary files /dev/null and b/images/5.jpg differ diff --git a/images/5.png b/images/5.png new file mode 100644 index 0000000..90035e6 Binary files /dev/null and b/images/5.png differ diff --git a/images/50.png b/images/50.png new file mode 100644 index 0000000..5ec6726 Binary files /dev/null and b/images/50.png differ diff --git a/images/51.png b/images/51.png new file mode 100644 index 0000000..25bbbfd Binary files /dev/null and b/images/51.png differ diff --git a/images/52.png b/images/52.png new file mode 100644 index 0000000..3d945a1 Binary files /dev/null and b/images/52.png differ diff --git a/images/53.png b/images/53.png new file mode 100644 index 0000000..d38f800 Binary files /dev/null and b/images/53.png differ diff --git a/images/54.png b/images/54.png new file mode 100644 index 0000000..c578502 Binary files /dev/null and b/images/54.png differ diff --git a/images/55.png b/images/55.png new file mode 100644 index 0000000..8e8875d Binary files /dev/null and b/images/55.png differ diff --git a/images/56.png b/images/56.png new file mode 100644 index 0000000..884d948 Binary files /dev/null and b/images/56.png differ diff --git a/images/57.png b/images/57.png new file mode 100644 index 0000000..78a571e Binary files /dev/null and b/images/57.png differ diff --git a/images/58.png b/images/58.png new file mode 100644 index 0000000..679a885 Binary files /dev/null and b/images/58.png differ diff --git a/images/59.png b/images/59.png new file mode 100644 index 0000000..0bab104 Binary files /dev/null and b/images/59.png differ diff --git a/images/6.gif b/images/6.gif new file mode 100644 index 0000000..6ba34e2 Binary files /dev/null and b/images/6.gif differ diff --git a/images/6.jpg b/images/6.jpg new file mode 100644 index 0000000..4986d34 Binary files /dev/null and b/images/6.jpg differ diff --git a/images/6.png b/images/6.png new file mode 100644 index 0000000..bad13a8 Binary files /dev/null and b/images/6.png differ diff --git a/images/60.png b/images/60.png new file mode 100644 index 0000000..7848fd9 Binary files /dev/null and b/images/60.png differ diff --git a/images/61.png b/images/61.png new file mode 100644 index 0000000..a0a4f3f Binary files /dev/null and b/images/61.png differ diff --git a/images/62.png b/images/62.png new file mode 100644 index 0000000..0dfaae4 Binary files /dev/null and b/images/62.png differ diff --git a/images/63.png b/images/63.png new file mode 100644 index 0000000..fbac5be Binary files /dev/null and b/images/63.png differ diff --git a/images/64.png b/images/64.png new file mode 100644 index 0000000..708ae07 Binary files /dev/null and b/images/64.png differ diff --git a/images/65.png b/images/65.png new file mode 100644 index 0000000..dc99db8 Binary files /dev/null and b/images/65.png differ diff --git a/images/66.png b/images/66.png new file mode 100644 index 0000000..d3920e1 Binary files /dev/null and b/images/66.png differ diff --git a/images/67.png b/images/67.png new file mode 100644 index 0000000..c2dde11 Binary files /dev/null and b/images/67.png differ diff --git a/images/68.png b/images/68.png new file mode 100644 index 0000000..f7066e1 Binary files /dev/null and b/images/68.png differ diff --git a/images/7.gif b/images/7.gif new file mode 100644 index 0000000..0b02c01 Binary files /dev/null and b/images/7.gif differ diff --git a/images/7.jpg b/images/7.jpg new file mode 100644 index 0000000..9d2f46c Binary files /dev/null and b/images/7.jpg differ diff --git a/images/7.png b/images/7.png new file mode 100644 index 0000000..5260253 Binary files /dev/null and b/images/7.png differ diff --git a/images/8.gif b/images/8.gif new file mode 100644 index 0000000..308cce0 Binary files /dev/null and b/images/8.gif differ diff --git a/images/8.jpg b/images/8.jpg new file mode 100644 index 0000000..4ae262d Binary files /dev/null and b/images/8.jpg differ diff --git a/images/8.png b/images/8.png new file mode 100644 index 0000000..1720fa5 Binary files /dev/null and b/images/8.png differ diff --git a/images/9.gif b/images/9.gif new file mode 100644 index 0000000..2671173 Binary files /dev/null and b/images/9.gif differ diff --git a/images/9.jpg b/images/9.jpg new file mode 100644 index 0000000..6035644 Binary files /dev/null and b/images/9.jpg differ diff --git a/images/9.png b/images/9.png new file mode 100644 index 0000000..bfae3b4 Binary files /dev/null and b/images/9.png differ diff --git a/io.md b/io.md new file mode 100644 index 0000000..d0b3c54 --- /dev/null +++ b/io.md @@ -0,0 +1,280 @@ +# 输入输出流 + +# Java 输入输出流详解 + +通过数据流、序列化和文件系统提供系统输入和输出。 +Java 把这些不同来源和目标的数据都统一抽象为数据流。Java 语言的输入输出功能是十分强大而灵活的,美中不足的是看上去输入输出的代码并不是很简洁,因为你往往需要包装许多不同的对象。 +在 Java 类库中,IO 部分的内容是很庞大的,因为它涉及的领域很广泛:标准输入输出,文件的操作,网络上的数据流,字符串流,对象流,zip 文件流。 +## 1.1、Java 流的分类 +按流向分: +输入流: 程序可以从中读取数据的流。 +输出流: 程序能向其中写入数据的流。 +按数据传输单位分: +字节流: 以字节为单位传输数据的流 +字符流: 以字符为单位传输数据的流 +按功能分: +节点流: 用于直接操作目标设备的流 +过滤流: 是对一个已存在的流的链接和封装,通过对数据进行处理为程序提供功能强大、灵活的读写功能。 +## 1.2、java.io 常用类 +JDK 所提供的所有流类位于 java.io 包中,都分别继承自以下四种抽象流类: +InputStream:继承自 InputStream 的流都是用于向程序中输入数据的,且数据单位都是字节(8位)。 +OutputSteam:继承自 OutputStream 的流都是程序用于向外输出数据的,且数据单位都是字节(8位)。 +Reader:继承自 Reader 的流都是用于向程序中输入数据的,且数据单位都是字符(16位)。 +Writer:继承自 Writer 的流都是程序用于向外输出数据的,且数据单位都是字符(16位)。 + +java 语言的输入输出功能是十分强大而灵活的,美中不足的是看上去输入输出的代码并不是很简洁,因为你往往需要包装许多不同的对象。在 Java 类库中,IO 部分的内容是很庞大的,因为它涉及的领域很广泛:标准输入输出,文件的操作,网络上的数据流,字符串流,对象流,zip 文件流....本文的目的是为大家做一个简要的介绍。 +流是一个很形象的概念,当程序需要读取数据的时候,就会开启一个通向数据源的流,这个数据源可以是文件,内存,或是网络连接。类似的,当程序需要写入数据的时候,就会开启一个通向目的地的流。这时候你就可以想象数据好像在这其中“流”动一样,如下图: + +![](images/5.gif) + +Java 中的流分为两种,一种是字节流,另一种是字符流,分别由四个抽象类来表示(每种流包括输入和输出两种所以一共四个):InputStream,OutputStream,Reader,Writer。Java 中其他多种多样变化的流均是由它们派生出来的: +stream 代表的是任何有能力产出数据的数据源,或是任何有能力接收数据的接收源。在 Java 的 IO 中,所有的 stream(包括 Inputstream 和 Out stream)都包括两种类型: +(1)字节流 +表示以字节为单位从 stream 中读取或往 stream 中写入信息,即 io 包中的 inputstream 类和 outputstream 类的派生类。通常用来读取二进制数据,如图象和声音。 +(2)字符流 +以 Unicode 字符为导向的 stream,表示以 Unicode 字符为单位从 stream 中读取或往 stream 中写入信息。 +区别: +Reader 和 Writer 要解决的,最主要的问题就是国际化。原先的 I/O 类库只支持8位的字节流,因此不可能很好地处理16位的 Unicode 字符流。Unicode 是国际化的字符集(更何况 Java 内置的 char 就是16位的 Unicode 字符),这样加了 Reader 和 Writer 之后,所有的 I/O 就都支持 Unicode 了。此外新类库的性能也比旧的好。 +但是,Read 和 Write 并不是取代 InputStream 和 OutputStream,有时,你还必须同时使用"基于 byte 的类"和"基于字符的类"。为此,它还提供了两个"适配器(adapter)"类。InputStreamReader 负责将 InputStream 转化成 Reader,而 OutputStreamWriter 则将 OutputStream 转化成 Writer。 +一.流的层次结构 +定义: +(1)java 将读取数据对象成为输入流,能向其写入的对象叫输出流。结构图如下: + +![](images/6.gif) + +输入流 + +![](images/7.gif) + +输出流 + +二.InputStream 类 +inputstream 类和 outputstream 类都为抽象类,不能创建对象,可以通过子类来实例化。 +InputStream 是输入字节数据用的类,所以 InputStream 类提供了3种重载的 read 方法.Inputstream 类中的常用方法: +(1)public abstract int read( ):读取一个 byte 的数据,返回值是高位补0的 int 类型值。 +(2)public int read(byte b[ ]):读取 b.length 个字节的数据放到b数组中。返回值是读取的字节数。该方法实际上是调用下一个方法实现的 +(3)public int read(byte b[ ], int off, int len):从输入流中最多读取 len 个字节的数据,存放到偏移量为 off 的 b 数组中。 +(4)public int available( ):返回输入流中可以读取的字节数。注意:若输入阻塞,当前线程将被挂起,如果 InputStream 对象调用这个方法的话,它只会返回0,这个方法必须由继承 InputStream 类的子类对象调用才有用, +(5)public long skip(long n):忽略输入流中的 n 个字节,返回值是实际忽略的字节数, 跳过一些字节来读取 +(6)public int close( ) :我们在使用完后,必须对我们打开的流进行关闭. +三.OutputStream 类 +OutputStream 提供了3个 write 方法来做数据的输出,这个是和 InputStream 是相对应的。 +1. public void write(byte b[ ]):将参数 b 中的字节写到输出流。 +2. public void write(byte b[ ], int off, int len) :将参数 b 的从偏移量 off 开始的 len 个字节写到输出流。 +3. public abstract void write(int b) :先将 int 转换为 byte 类型,把低字节写入到输出流中。 +4. public void flush( ) : 将数据缓冲区中数据全部输出,并清空缓冲区。 +5. public void close( ) : 关闭输出流并释放与流相关的系统资源。 +注意: +1. 上述各方法都有可能引起异常。 +2. InputStream 和 OutputStream 都是抽象类,不能创建这种类型的对象。 +四.FileInputStream 类 +FileInputStream 类是 InputStream 类的子类,用来处理以文件作为数据输入源的数据流。使用方法: +方式1: + +``` +File fin=new File("d:/abc.txt"); +FileInputStream in=new FileInputStream(fin); +``` + +方式2: + +``` +FileInputStream in=new +FileInputStream("d: /abc.txt"); +``` + +方式3: +构造函数将 FileDescriptor()对象作为其参数。 + +``` +FileDescriptor() fd=new FileDescriptor(); +FileInputStream f2=new FileInputStream(fd); +``` + +五.FileOutputStream 类 +FileOutputStream 类用来处理以文件作为数据输出目的数据流;一个表示文件名的字符串,也可以是 File 或 FileDescriptor 对象。 +创建一个文件流对象有两种方法: +方式1: + +``` +File f=new File("d:/abc.txt"); +FileOutputStream out=new FileOutputStream (f); +``` + +方式2: + +``` +FileOutputStream out=new +FileOutputStream("d:/abc.txt"); +``` + +方式3:构造函数将 FileDescriptor()对象作为其参数。 + +``` +FileDescriptor() fd=new FileDescriptor(); +FileOutputStream f2=new FileOutputStream(fd); +``` + +方式4:构造函数将文件名作为其第一参数,将布尔值作为第二参数。 + +``` +FileOutputStream f=new FileOutputStream("d:/abc.txt",true); +``` + +注意: +(1)文件中写数据时,若文件已经存在,则覆盖存在的文件;(2)的读/写操作结束时,应调用 close 方法关闭流。 +举例:2-1 +六.File 类 +File 类与 InputStream / OutputStream 类同属于一个包,它不允许访问文件内容。 +File 类主要用于命名文件、查询文件属性和处理文件目录。 +举例:2-2 +七.从一个流构造另一个流 +java 的流类提供了结构化方法,如,底层流和高层过滤流。 +而高层流不是从输入设备读取,而是从其他流读取。同样高层输出流也不是写入输出设备,而是写入其他流。 +使用"分层对象(layered objects)",为单个对象动态地,透明地添加功能的做法,被称为 Decorator Pattern。Decorator 模式要求所有包覆在原始对象之外的对象,都必须具有与之完全相同的接口。这使得 decorator 的用法变得非常的透明--无论对象是否被 decorate 过,传给它的消息总是相同的。这也是 Java I/O 类库要有"filter(过滤器)"类的原因:抽象的"filter"类是所有 decorator 的基类。Decorator 模式常用于如下的情形:如果用继承来解决各种需求的话,类的数量会多到不切实际的地步。Java 的 I/O 类库需要提供很多功能的组合,于是 decorator 模式就有了用武之地。 +为 InputStream 和 OutputStream 定义 decorator 类接口的类,分别是 FilterInputStream 和 FilterOutputStream。 + +![](images/66.png) + +![](images/67.png) + +DataInputStream 类对象可以读取各种类型的数据。 +DataOutputStream 类对象可以写各种类型的数据; +创建这两类对象时,必须使新建立的对象指向构造函数中的参数对象。例如: + +``` +FileInputStream in=new FileInputStream("d:/abc.txt"); +DataInputStream din=new DataInputStream(in); +``` + +7.2BufferInputStream 和 bufferOutputStream +允许程序在不降低系统性能的情况下一次一个字节的从流中读取数据。 +BufferInputstream 定义了两种构造函数 +(1)BufferInputStream b= new BufferInputstream(in); +(2)BufferInputStream b=new BufferInputStream(in,size) +第二个参数表示指定缓冲器的大小。 +同样 BufferOutputStream 也有两种构造函数。一次一个字节的向流中写数据。 +7.3printstream +用于写入文本或基本类型 +两种构造函数方法: +PrintStream ps=new PrintStream(out); +PrintStream ps=new PrintStream(out, autoflush) +第二个参数为布尔值,控制每次输出换行符时 java 是否刷新输出流。 +八.字符流的读取和写入 +java.io.Reader 和 java.io.InputStream 组成了 Java 输入类。Reader 用于读入16位字符,也就是 Unicode 编码的字符;而 InputStream 用于读入 ASCII 字符和二进制数据。 + +![](images/10.gif) + +Reader 体系结构 +(1)FileReader +FileReader 主要用来读取字符文件,使用缺省的字符编码,有三种构造函数: +--将文件名作为字符串 + +``` +FileReader f=new FileReader(“c:/temp.txt”); +``` + +--构造函数将 File 对象作为其参数。 + +``` +File f=new file(“c:/temp.txt”); +FileReader f1=new FileReader(f); +``` + +--构造函数将 FileDescriptor 对象作为参数 + +``` +FileDescriptor() fd=new FileDescriptor() +FileReader f2=new FileReader(fd); +``` + +(2)charArrayReader +将字符数组作为输入流,构造函数为: + +``` +public CharArrayReader(char[] ch); +``` + +(3)StringReader +读取字符串,构造函数如下: + +``` +public StringReader(String s); +``` + +(4)InputStreamReader +从输入流读取字节,在将它们转换成字符。 + +``` +Public inputstreamReader(inputstream is); +``` + +(5)FilterReader +允许过滤字符流 + +``` +protected filterReader(Reader r); +``` + +(6)BufferReader +接受 Reader 对象作为参数,并对其添加字符缓冲器,使用readline()方法可以读取一行。 + +``` +Public BufferReader(Reader r); +``` + +![](images/11.gif) + +Writer 类体系结构 +(1)FileWrite +将字符类型数据写入文件,使用缺省字符编码和缓冲器大小。 + +``` +Public FileWrite(file f); +``` + +(2)chararrayWrite() +将字符缓冲器用作输出。 + +``` +Public CharArrayWrite(); +``` + +(3)PrintWrite +生成格式化输出 + +``` +public PrintWriter(outputstream os); +``` + +(4)filterWriter +用于写入过滤字符流 + +``` +protected FilterWriter(Writer w); +``` + +![](images/68.png) + +DataInputStream 类对象可以读取各种类型的数据。 +DataOutputStream 类对象可以写各种类型的数据; +创建这两类对象时,必须使新建立的对象指向构造函数中的参数对象。例如: + +``` +FileInputStream in=new FileInputStream("d:/abc.txt"); +DataInputStream din=new DataInputStream(in); +``` + +7.2BufferInputStream 和 bufferOutputStream +允许程序在不降低系统性能的情况下一次一个字节的从流中读取数据。 +BufferInputstream 定义了两种构造函数 +(1)BufferInputStream b= new BufferInputstream(in); +(2)BufferInputStream b=new BufferInputStream(in,size) +第二个参数表示指定缓冲器的大小。 +同样 BufferOutputStream 也有两种构造函数。一次一个字节的向流中写数据。 +7.3printstream +用于写入文本或基本类型 +两种构造函数方法: +PrintStream ps=new PrintStream(out); +PrintStream ps=new PrintStream(out, autoflush) +第二个参数为布尔值,控制每次输出换行符时 java 是否刷新输出流 \ No newline at end of file diff --git a/jdbc.md b/jdbc.md new file mode 100644 index 0000000..71f9863 --- /dev/null +++ b/jdbc.md @@ -0,0 +1,464 @@ +# JDBC + +什么是 JDBC? +Java 语言访问数据库的一种规范,是一套 API +JDBC (Java Database Connectivity) API,即 Java 数据库编程接口,是一组标准的 Java 语言中的接口和类,使用这些接口和类,Java 客户端程序可以访问各种不同类型的数据库。比如建立数据库连接、执行 SQL 语句进行数据的存取操作。 + +JDBC 规范采用接口和实现分离的思想设计了 Java 数据库编程的框架。接口包含在 java.sql 及 javax.sql 包中,其中 java.sql 属于 JavaSE,javax.sql 属于 JavaEE。这些接口的实现类叫做数据库驱动程序,由数据库的厂商或其它的厂商或个人提供。 + +为了使客户端程序独立于特定的数据库驱动程序,JDBC 规范建议开发者使用基于接口的编程方式,即尽量使应用仅依赖 java.sql 及 javax.sql 中的接口和类。 + +![](images/36.jpg) + +JDBC 驱动程序: +什么是 JDBC 驱动程序? +这些是各个数据库厂家根据 JDBC 的规范制作的 JDBC 实现类 +JDBC 驱动程序的四种类型: +1. 第一种类型的驱动程序的实现是通过将 JDBC 的调用全部委托给其它编程接口来实现的,比如 ODBC。这种类型的驱动程序需要安装本地代码库,即依赖于本地的程序,所以便携性较差。比如 JDBC-ODBC 桥驱动程序 +2. 第二种类型的驱动程序的实现是部分基于 Java 语言的。即该驱动程序一部分是用 Java 语言编写,其它部分委托本地的数据库的客户端代码来实现。同类型1的驱动一样,该类型的驱动程序也依赖本地的程序,所以便携性较差 +3. 第三种类型的驱动程序的实现是全部基于 JAVA 语言的。该类型的驱动程序通常由某个中间件服务器提供,这样客户端程序可以使用数据库无关的协议和中间件服务器进行通信,中间件服务器再将客户端的 JDBC 调用转发给数据库进行处理 +4. 第四种类型的驱动程序的实现是全部基于 JAVA 语言的。该类型的驱动程序中包含了特定数据库的访问协议,使得客户端可以直接和数据库进行通信 + +JDBC 类结构: + +![](images/37.jpg) + +DriverManager:这个是一个实现类,它是一个工厂类,用来生产 Driver 对象的 +这个类的结构设计模式为工厂方法 +Driver:这是驱动程序对象的接口,它指向一个实实在在的数据库驱动程序对象,那么这个数据库驱动程序对象是从哪里来的呢? +DriverManager 工厂中有个方法:getDriver(String URL),通过这个方法可以得到驱动程序对象,这个方法是在各个数据库厂商按JDBC规范设计的数据库驱动程序包里的类中静态实现的,也就是在静态块中 +Connection:这个接口可以制向一个数据库连接对象,那么如何得到这个连接对象呢? +是通过 DriverManager 工厂中的 getConnection(String URL)方法得到的 +Statement:用于执行静态的 SQL 语句的接口,通过 Connection 中的 createStatement 方法得到的 +Resultset:用于指向结果集对象的接口,结果集对象是通过 Statement 中的 execute 等方法得到的 + +JAVA 使用 JDBC 访问数据库的步骤: +1. 得到数据库驱动程序 +2. 创建数据库连接 +3. 执行 SQL 语句 +4. 得到结果集 +5. 对结果集做相应的处理(增,删,改,查) +6. 关闭资源:这里释放的是 DB 中的资源 + +![](images/38.jpg) + +设置 classpath: +1. 在 java 文件中起的包名一定要是工程基目录下的子目录,classpath:基目录 +2. .jar 包,需要将这个 .jar 包的路径包括这个文件的全名添加到 classpath 中来 +Oracle 连接字符串的书写格式: +“oracle:jdbc:thin:@ip:1521: 数据库名”,”数据库用户名”,”数据库密码” + +简单的例子: + +``` +package moudule1.first; + +import java.sql.*; + +public class FirstJdbc +{ + public static void main(String[] args) + { + String sql="select * from yuchen_user"; + Connection con=null; + Statement st=null; + ResultSet rs=null; + + try + { + Class.forName("oracle.jdbc.driver.OracleDriver"); + con=DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:name", "scott","tiger"); + st=con.createStatement(); + rs=st.executeQuery(sql); + while(rs.next()) + { + System.out.println(rs.getInt("id")); + System.out.println(rs.getString("name")); + } + }catch(Exception e) + { + e.printStackTrace(); + }finally + { + try + { + con.close(); + }catch(Exception e) + {} + + try + { + st.close(); + }catch(Exception e) + { + } + + try + { + rs.close(); + }catch(Exception e) + { + } + + } + + + } + +} +``` + +常用数据库的驱动程序及 JDBC URL: +Oracle 数据库: +驱动程序包名:ojdbc14.jar +驱动类的名字:oracle.jdbc.driver.OracleDriver +JDBC URL:jdbc:oracle:thin:@dbip:port:databasename +说明:驱动程序包名有可能会变 +JDBC URL 中黑色字体部分必须原封不动的保留,为该驱动识别的 URL 格式。红色字体部分需要根据数据库的安装情况填写。其中各个部分含义如下: +dbip –为数据库服务器的 IP 地址,如果是本地可写:localhost 或 127.0.0.1。 +port –为数据库的监听端口,需要看安装时的配置,缺省为1521。 +databasename –为数据库的 SID,通常为全局数据库的名字。 +举例如果要访问本地的数据库 allandb,端口1521,那么 URL 写法如下: +jdbc:oracle:thin:@localhost:1521:allandb 下载地址如下: +http://www.oracle.com/technology/software/tech/java/sqlj_jdbc/index.html + +SQL Server 数据库 +驱动程序包名:msbase.jar mssqlserver.jar msutil.jar +驱动类的名字:com.microsoft.jdbc.sqlserver.SQLServerDriver +JDBC URL:jdbc:microsoft:sqlserver://dbip:port;DatabaseName=databasename +说明:驱动程序包名有可能会变 +JDBC URL 中黑色字体部分必须原封不动的保留,为该驱动识别的 URL 格式。红色字体部需要根据数据库的安装情况填写。其中各个部分含义如下: +dbip –为数据库服务器的 IP 地址,如果是本地可写:localhost 或 127.0.0.1。 +port –为数据库的监听端口,需要看安装时的配置,缺省为1433。 +databasename –数据库的名字。 +举例如果要访问本地的数据库 allandb,端口1433,那么 URL 写法如下: +jdbc: microsoft: sqlserver:@localhost:1433; DatabaseName =allandb +下载地址:http://www.microsoft.com/downloads/details.aspx + +MySQL 数据库 +驱动程序包名:mysql-connector-java-3.1.11-bin.jar +驱动类的名字:com.mysql.jdbc.Driver +JDBC URL:jdbc:mysql://dbip:port/databasename +说明:驱动程序包名有可能会变 +JDBC URL 中黑色字体部分必须原封不动的保留,为该驱动识别的 URL 格式。红色字体部需要根据数据库的安装情况填写。其中各个部分含义如下: +dbip –为数据库服务器的 IP 地址,如果是本地可写:localhost 或 127.0.0.1。 +port –为数据库的监听端口,需要看安装时的配置,缺省为3306。 +databasename –数据库的名字。 +举例如果要访问本地的数据库 allandb,端口1433,那么 URL 写法如下: +jdbc:mysql://localhost:3306/allandb +下载地址:http://dev.mysql.com/downloads/connector/j/ + +Access 数据库 +驱动程序包名:该驱动程序包含在 JavaSE 中,不需要额外安装。 +驱动类的名字:sun.jdbc.odbc.JdbcOdbcDriver +JDBC URL:jdbc:odbc:datasourcename +说明:该驱动只能工作在 Windows 系统中,首先需要在操作系统中建立一个可以访问Access 数据库的本地数据源(ODBC),如果名字为 allandb,那么 URL 写法如下: +jdbc:odbc:allandb + +PreparedStatement 接口: +预编译的 sql 语句对象 +作用: 解决了书写 sql 语句时一些特殊的字符与 sql 保留字符冲突的问题,非常方便 + +``` +/** +*知识点: +*PreparedStatement 接口及方法的使用 +*程序目标: +*java文件: +*PreparedInsert.java:连接数据库,插入一条数据 +*JdbcUtil.java:实现一个工具类,功能:1.连接数据库 2.关闭资源 +*/ +package moudule1.preparedstatement; + +import java.sql.*; +import moudule1.com.*; + +public class PreparedInsert +{ + public static void main(String[] args) + { + String sql="insert into yuchen_user (id,name) values (?,?)"; + System.out.println(sql); + + Connection con=null; + PreparedStatement ps=null; + + try{ + con=JdbcUtil.getConnection(); + ps=con.prepareStatement(sql); + + ps.setInt(1,2); + ps.setString(2,"zhangsan"); + ps.executeUpdate(); + + ps.setInt(1,3); + ps.setString(2,"lisi"); + ps.executeUpdate(); + + }catch(Exception e){ + e.printStackTrace(); + }finally{ + JdbcUtil.close(con,ps); + } + } + } + +package moudule1.com; + +import java.sql.*; + +public class JdbcUtil{ + + public static Connection getConnection() throws Exception{ + Class.forName("oracle.jdbc.driver.OracleDriver"); + return DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:name", "scott","tiger"); + } + + public static void close(Connection con,Statement st){ + + close(con); + close(st); + + } + + public static void close(Connection con,Statement st,ResultSet rs){ + + + close(con,st); + close(rs); + + } + + public static void close(Connection con){ + try{ + + con.close(); + + }catch(Exception e){ + + + } + + } + + public static void close(Statement st){ + + try{ + + st.close(); + + }catch(Exception e){ + + } + } + + public static void close(ResultSet rs){ + + try{ + + rs.close(); + + }catch(Exception e){ + + } + + } + + } + +数据库的增删改查的例子: +/** +*知识点: +*JDBC+SQL+ORACLE +*程序目标: +*UserDao.java:实现了数据库的增删改查 +*JdbcUtil.java:工具类,有连库和关闭资源的方法 +*/ + +package moudule1.idus; + +import java.sql.*; +import moudule1.com.*; + +public class UserDao{ + + private String sql; + private Connection con; + private Statement st; + private ResultSet rs; + + public UserDao(){ + sql=null; + con=null; + st=null; + rs=null; + } + + public void insert(){ + + sql="insert into yuchen_user (id,name) values("; + sql+="4,'zhouwu')"; + System.out.println(sql); + + try{ + + con=JdbcUtil.getConnection(); + st=con.createStatement(); + st.executeUpdate(sql); + + }catch(Exception e){ + + e.printStackTrace(); + + }finally{ + + JdbcUtil.close(con,st); + + } + + } + + + public void delete(){ + + sql="delete from yuchen_user where id=2"; + System.out.println(sql); + + try{ + + con=JdbcUtil.getConnection(); + st=con.createStatement(); + st.executeUpdate(sql); + + }catch(Exception e){ + + e.printStackTrace(); + + }finally{ + + JdbcUtil.close(con,st); + + } + } + + + public void update(){ + + sql="update yuchen_user set name='liumang' where id=1"; + System.out.println(sql); + + try{ + + con=JdbcUtil.getConnection(); + st=con.createStatement(); + st.executeUpdate(sql); + + }catch(Exception e){ + + e.printStackTrace(); + + }finally{ + + JdbcUtil.close(con,st); + + } + } + + + public void select(){ + + sql="select * from yuchen_user"; + System.out.println(sql); + + try{ + + con=JdbcUtil.getConnection(); + st=con.createStatement(); + rs=st.executeQuery(sql); + + while(rs.next()){ + + System.out.println(rs.getInt(1)); + System.out.println(rs.getString(2)); + + } + + }catch(Exception e){ + + e.printStackTrace(); + + }finally{ + + JdbcUtil.close(con,st,rs); + + } + + } + + + public static void main(String[] args){ + + UserDao ud=new UserDao(); + ud.select(); + ud.insert(); + ud.select(); + ud.update(); + ud.select(); + ud.delete(); + ud.select(); + } + } + +一些常用的方法: +/** +*知识点: +*execute方法,getResultSet(),getUpdateCount() +*程序目标: +*JdbcUtil.java:工具类,连接数据库,关闭资源 +*sqlExecutor.java:命令行参数输入sql语句,并执行该语句 +*/ +package moudule1.fangfa; + +import java.sql.*; +import moudule1.com.*; + +public class sqlExecutor{ + + public static void main(String[] args){ + + Connection con=null; + Statement st=null; + + try{ + + con=JdbcUtil.getConnection(); + st=con.createStatement(); + boolean str=st.execute(args[0]); + + if(str){ + + ResultSet rs=st.getResultSet(); + + while(rs.next()){ + System.out.println(rs.getInt("id")+":"+rs.getString("name")); + } + + rs.close(); + + }else{ + int row=st.getUpdateCount(); + System.out.println(row); + } + + }catch(Exception e){ + + e.printStackTrace(); + + }finally{ + + JdbcUtil.close(con,st); + + } + } + } +``` \ No newline at end of file diff --git a/massive.md b/massive.md new file mode 100644 index 0000000..490bdbb --- /dev/null +++ b/massive.md @@ -0,0 +1,85 @@ +# 海量数据处理 + +# 常见的海量数据处理方法 + +1. 给定 a、b 两个文件,各存放50亿个 url,每个 url 各占64字节,内存限制是 4 G,让你找出 a、b 文件共同的 url? +方案1:可以估计每个文件安的大小为 50 G×64=320 G,远远大于内存限制的 4 G。所以不可能将其完全加载到内存中处理。考虑采取分而治之的方法。 +s 遍历文件 a,对每个 url 求取 ,然后根据所取得的值将 url 分别存储到1000个小文件(记为 )中。这样每个小文件的大约为 300 M。 +s 遍历文件 b,采取和 a 相同的方式将 url 分别存储到1000各小文件(记为 )。这样处理后,所有可能相同的 url 都在对应的小文件( )中,不对应的小文件不可能有相同的 url。然后我们只要求出1000对小文件中相同的 url 即可。 +s 求每对小文件中相同的 url 时,可以把其中一个小文件的 url 存储到 hash_set 中。然后遍历另一个小文件的每个 url,看其是否在刚才构建的 hash_set 中,如果是,那么就是共同的 url,存到文件里面就可以了。 +方案2:如果允许有一定的错误率,可以使用 Bloom filter,4 G 内存大概可以表示340亿 bit。将其中一个文件中的 url 使用 Bloom filter 映射为这340亿 bit,然后挨个读取另外一个文件的 url,检查是否与 Bloom filter,如果是,那么该 url 应该是共同的 url(注意会有一定的错误率)。 + +2. 有10个文件,每个文件 1 G,每个文件的每一行存放的都是用户的 query,每个文件的 query 都可能重复。要求你按照 query 的频度排序。 +方案1: +s 顺序读取10个文件,按照 hash(query)%10的结果将 query 写入到另外10个文件(记为 )中。这样新生成的文件每个的大小大约也 1 G(假设 hash 函数是随机的)。 +s 找一台内存在 2 G 左右的机器,依次对 用 hash_map(query, query_count)来统计每个 query 出现的次数。利用快速/堆/归并排序按照出现次数进行排序。将排序好的 query 和对应的 query_cout 输出到文件中。这样得到了10个排好序的文件(记为 )。 +s 对 这10个文件进行归并排序(内排序与外排序相结合)。 +方案2: +一般 query 的总量是有限的,只是重复的次数比较多而已,可能对于所有的 query,一次性就可以加入到内存了。这样,我们就可以采用 trie 树 /hash_map 等直接来统计每个 query 出现的次数,然后按出现次数做快速/堆/归并排序就可以了。 +方案3: +与方案1类似,但在做完 hash,分成多个文件后,可以交给多个文件来处理,采用分布式的架构来处理(比如 MapReduce),最后再进行合并。 + +3. 有一个 1 G 大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是 1 M。返回频数最高的100个词。 +方案1:顺序读文件中,对于每个词 x,取 ,然后按照该值存到5000个小文件(记为 ) 中。这样每个文件大概是 200 k 左右。如果其中的有的文件超过了 1 M 大小,还可以按照类似的方法继续往下分,知道分解得到的小文件的大小都不超过 1 M。对 每个小文件,统计每个文件中出现的词以及相应的频率(可以采用 trie 树 /hash_map 等),并取出出现频率最大的100个词(可以用含100个结点 的最小堆),并把100词及相应的频率存入文件,这样又得到了5000个文件。下一步就是把这5000个文件进行归并(类似与归并排序)的过程了。 + +4. 海量日志数据,提取出某日访问百度次数最多的那个 IP。 +方案1:首先是这一天,并且是访问百度的日志中的 IP 取出来,逐个写入到一个大文件中。注意到 IP 是32位的,最多有 个 IP。同样可以采用映射的方法,比如模1000,把整个大文件映射为1000个小文件,再找出每个小文中出现频率最大的 IP(可以采用 hash_map 进 行频率统计,然后再找出频率最大的几个)及相应的频率。然后再在这1000个最大的 IP 中,找出那个频率最大的 IP,即为所求。 + +5. 在2.5亿个整数中找出不重复的整数,内存不足以容纳这2.5亿个整数。 +方案1:采用2-Bitmap(每个数分配 2 bit,00表示不存在,01表示出现一次,10表示多次,11无意义)进行,共需内存 内存,还可以接受。然后扫描这2.5亿个整数,查看 Bitmap 中相对应位,如果是00变01,01变10,10保持不变。所描完事后,查看 bitmap,把对应位是01的整数输出即可。 +方案2:也可采用上题类似的方法,进行划分小文件的方法。然后在小文件中找出不重复的整数,并排序。然后再进行归并,注意去除重复的元素。 + +6. 海量数据分布在100台电脑中,想个办法高校统计出这批数据的 TOP10。 +方案1: +s 在每台电脑上求出 TOP10,可以采用包含10个元素的堆完成(TOP10 小,用最大堆,TOP10 大,用最小堆)。比如求 TOP10 大,我们首先取前 10个元素调整成最小堆,如果发现,然后扫描后面的数据,并与堆顶元素比较,如果比堆顶元素大,那么用该元素替换堆顶,然后再调整为最小堆。最后堆中的元 素就是 TOP10 大。 +s 求出每台电脑上的 TOP10 后,然后把这100台电脑上的 TOP10 组合起来,共1000个数据,再利用上面类似的方法求出 TOP10 就可以了。 + +7. 怎么在海量数据中找出重复次数最多的一个? +方案1:先做 hash,然后求模映射为小文件,求出每个小文件中重复次数最多的一个,并记录重复次数。然后找出上一步求出的数据中重复次数最多的一个就是所求(具体参考前面的题)。 + +8. 上千万或上亿数据(有重复),统计其中出现次数最多的钱 N 个数据。 +方案1:上千万或上亿的数据,现在的机器的内存应该能存下。所以考虑采用 hash_map/搜索二叉树/红黑树等来进行统计次数。然后就是取出前 N 个出现次数最多的数据了,可以用第6题提到的堆机制完成。 + +9. 1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。请怎么设计和实现? +方案1:这题用 trie 树比较合适,hash_map 也应该能行。 + +10. 一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。 +方案1:这题是考虑时间效率。用 trie 树统计每个词出现的次数,时间复杂度是 O(n*le)(le 表示单词的平准长度)。然后是找出出现最频繁的 前10个词,可以用堆来实现,前面的题中已经讲到了,时间复杂度是 O(n*lg10)。所以总的时间复杂度,是 O(n*le)与 O(n*lg10)中较大 的哪一个。 + +11. 一个文本文件,找出前10个经常出现的词,但这次文件比较长,说是上亿行或十亿行,总之无法一次读入内存,问最优解。 +方案1:首先根据用 hash 并求模,将文件分解为多个小文件,对于单个文件利用上题的方法求出每个文件件中10个最常出现的词。然后再进行归并处理,找出最终的10个最常出现的词。 + +12. 100 w 个数中找出最大的100个数。 +方案1:在前面的题中,我们已经提到了,用一个含100个元素的最小堆完成。复杂度为O(100 w*lg100)。 +方案2:采用快速排序的思想,每次分割之后只考虑比轴大的一部分,知道比轴大的一部分在比100多的时候,采用传统排序算法排序,取前100个。复杂度为O(100 w*100)。 +方案3:采用局部淘汰法。选取前100个元素,并排序,记为序列L。然后一次扫描剩余的元素x,与排好序的100个元素中最小的元素比,如果比这个 最小的要大,那么把这个最小的元素删除,并把 x 利用插入排序的思想,插入到序列 L 中。依次循环,知道扫描了所有的元素。复杂度为 O(100 w*100)。 + +13. 寻找热门查询: +搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复 读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个 查询串,要求使用的内存不能超过 1 G。 +(1) 请描述你解决这个问题的思路; +(2) 请给出主要的处理流程,算法,以及算法的复杂度。 +方案1:采用 trie 树,关键字域存该查询串出现的次数,没有出现为0。最后用10个元素的最小推来对出现频率进行排序。 + +14. 一共有 N 个机器,每个机器上有 N 个数。每个机器最多存 O(N)个数并对它们操作。如何找到 个数中的中数? +方案1:先大体估计一下这些数的范围,比如这里假设这些数都是32位无符号整数(共有 个)。我们把0到 的整数划分为 N 个范围段,每个段包含 个整数。比如,第一个段位0到 ,第二段为 到 ,…,第 N 个段为 到 。 然后,扫描每个机器上的 N 个数,把属于第一个区段的数放到第一个机器上,属于第二个区段的数放到第二个机器上,…,属于第 N 个区段的数放到第 N 个机器上。 注意这个过程每个机器上存储的数应该是 O(N)的。下面我们依次统计每个机器上数的个数,一次累加,直到找到第 k 个机器,在该机器上累加的数大于或等于 ,而在第 k-1 个机器上的累加数小于 ,并把这个数记为 x。那么我们要找的中位数在第 k 个机器中,排在第 位。然后我们对第 k 个机器的数排序,并找出第 个数,即为所求的中位数。复杂度是 的。 +方案2:先对每台机器上的数进行排序。排好序后,我们采用归并排序的思想,将这 N 个机器上的数归并起来得到最终的排序。找到第 n 个便是所求。复杂度是 n(i)的。 + +15. 最大间隙问题 +给定 n 个实数 ,求着 n 个实数在实轴上向量2个数之间的最大差值,要求线性的时间算法。 +方案1:最先想到的方法就是先对这 n 个数据进行排序,然后一遍扫描即可确定相邻的最大间隙。但该方法不能满足线性时间的要求。故采取如下方法: +s 找到 n 个数据中最大和最小数据 max 和 min。 +s 用 n-2 个点等分区间[min, max],即将[min, max]等分为 n-1 个区间(前闭后开区间),将这些区间看作桶,编号为 ,且桶 的上界和桶 i+1 的下届相同,即每个桶的大小相同。每个桶的大小为: 。实际上,这些桶的边界构成了一个等差数列(首项为 min,公差为 ),且认为将 min 放入第一个桶,将 max 放入第 n-1 个桶。 +s 将 n 个数放入 n-1 个桶中:将每个元素 分配到某个桶(编号为 index),其中 ,并求出分到每个桶的最大最小数据。 +s 最大间隙:除最大最小数据 max 和 min 以外的 n-2 个数据放入 n-1 个桶中,由抽屉原理可知至少有一个桶是空的,又因为每个桶的大小相同,所以最大间隙 不会在同一桶中出现,一定是某个桶的上界和气候某个桶的下界之间隙,且该量筒之间的桶(即便好在该连个便好之间的桶)一定是空桶。也就是说,最大间隙在桶 i 的上界和桶 j 的下界之间产生 。一遍扫描即可完成。 + +16. 将多个集合合并成没有交集的集合:给定一个字符串的集合,格式如: 。要求将其中交集不为空的集合合并,要求合并完成的集合之间无交集,例如上例应输出 。 +(1) 请描述你解决这个问题的思路; +(2) 给出主要的处理流程,算法,以及算法的复杂度; +(3) 请描述可能的改进。 +方案1:采用并查集。首先所有的字符串都在单独的并查集中。然后依扫描每个集合,顺序合并将两个相邻元素合并。例如,对于 , 首先查看 aaa 和 bbb 是否在同一个并查集中,如果不在,那么把它们所在的并查集合并,然后再看 bbb 和 ccc 是否在同一个并查集中,如果不在,那么也把 它们所在的并查集合并。接下来再扫描其他的集合,当所有的集合都扫描完了,并查集代表的集合便是所求。复杂度应该是 O(NlgN)的。改进的话,首先可以 记录每个节点的根结点,改进查询。合并的时候,可以把大的和小的进行合,这样也减少复杂度。 + +17. 最大子序列与最大子矩阵问题 +数组的最大子序列问题:给定一个数组,其中元素有正,也有负,找出其中一个连续子序列,使和最大。 +方案1:这个问题可以动态规划的思想解决。设 表示以第 i 个元素 结尾的最大子序列,那么显然 。基于这一点可以很快用代码实现。 +最大子矩阵问题:给定一个矩阵(二维数组),其中数据有大有小,请找一个子矩阵,使得子矩阵的和最大,并输出这个和。 +方案1:可以采用与最大子序列类似的思想来解决。如果我们确定了选择第 i 列和第 j 列之间的元素,那么在这个范围内,其实就是一个最大子序列问题。如何确定第i列和第j列可以词用暴搜的方法进行。 \ No newline at end of file diff --git a/multi-thread.md b/multi-thread.md new file mode 100644 index 0000000..a814741 --- /dev/null +++ b/multi-thread.md @@ -0,0 +1,1389 @@ +# Java 多线程 + +# java 中的多线程 + +在 java 中要想实现多线程,有两种手段,一种是继续 Thread 类,另外一种是实现 Runable 接口。 + +对于直接继承 Thread 的类来说,代码大致框架是: + +``` +class 类名 extends Thread{ +方法1; +方法2; +… +public void run(){ +// other code… +} +属性1; +属性2; +… + +} +``` + +先看一个简单的例子: + +``` +/** + * @author Rollen-Holt 继承Thread类,直接调用run方法 + * */ +class hello extends Thread { + + public hello() { + + } + + public hello(String name) { + this.name = name; + } + + public void run() { + for (int i = 0; i < 5; i++) { + System.out.println(name + "运行 " + i); + } + } + + public static void main(String[] args) { + hello h1=new hello("A"); + hello h2=new hello("B"); + h1.run(); + h2.run(); + } + + private String name; +} +``` + +【运行结果】: + +A 运行 0 + +A 运行 1 + +A 运行 2 + +A 运行 3 + +A 运行 4 + +B 运行 0 + +B 运行 1 + +B 运行 2 + +B 运行 3 + +B 运行 4 + +我们会发现这些都是顺序执行的,说明我们的调用方法不对,应该调用的是 start()方法。 + +当我们把上面的主函数修改为如下所示的时候: + +``` +public static void main(String[] args) { + hello h1=new hello("A"); + hello h2=new hello("B"); + h1.start(); + h2.start(); + } +``` + +然后运行程序,输出的可能的结果如下: + +A 运行 0 + +B 运行 0 + +B 运行 1 + +B 运行 2 + +B 运行 3 + +B 运行 4 + +A 运行 1 + +A 运行 2 + +A 运行 3 + +A 运行 4 + +因为需要用到 CPU 的资源,所以每次的运行结果基本是都不一样的,呵呵。 + +注意:虽然我们在这里调用的是 start()方法,但是实际上调用的还是 run()方法的主体。 + +那么:为什么我们不能直接调用 run()方法呢? + +我的理解是:线程的运行需要本地操作系统的支持。 + +如果你查看 start 的源代码的时候,会发现: + +``` +public synchronized void start() { + /** + * This method is not invoked for the main method thread or "system" + * group threads created/set up by the VM. Any new functionality added + * to this method in the future may have to also be added to the VM. + * + * A zero status value corresponds to state "NEW". + */ + if (threadStatus != 0 || this != me) + throw new IllegalThreadStateException(); + group.add(this); + start0(); + if (stopBeforeStart) { + stop0(throwableFromStop); + } +} +private native void start0(); +``` + +注意我用红色加粗的那一条语句,说明此处调用的是 start0()。并且这个这个方法用了 native 关键字,次关键字表示调用本地操作系统的函数。因为多线程的实现需要本地操作系统的支持。 + +但是 start 方法重复调用的话,会出现 java.lang.IllegalThreadStateException 异常。 + +通过实现 Runnable 接口: + +大致框架是: + +``` +class 类名 implements Runnable{ +方法1; +方法2; +… +public void run(){ +// other code… +} +属性1; +属性2; +… + +} +``` + +来先看一个小例子吧: + +``` +/** + * @author Rollen-Holt 实现Runnable接口 + * */ +class hello implements Runnable { + + public hello() { + + } + + public hello(String name) { + this.name = name; + } + + public void run() { + for (int i = 0; i < 5; i++) { + System.out.println(name + "运行 " + i); + } + } + + public static void main(String[] args) { + hello h1=new hello("线程A"); + Thread demo= new Thread(h1); + hello h2=new hello("线程B"); + Thread demo1=new Thread(h2); + demo.start(); + demo1.start(); + } + + private String name; +} +``` + +【可能的运行结果】: + +线程 A 运行 0 + +线程 B 运行 0 + +线程 B 运行 1 + +线程 B 运行 2 + +线程 B 运行 3 + +线程 B 运行 4 + +线程 A 运行 1 + +线程 A 运行 2 + +线程 A 运行 3 + +线程 A 运行 4 + + +关于选择继承 Thread 还是实现 Runnable 接口? + +其实 Thread 也是实现 Runnable 接口的: + +``` +class Thread implements Runnable { + //… +public void run() { + if (target != null) { + target.run(); + } + } +} +``` + +其实 Thread 中的 run 方法调用的是 Runnable 接口的 run 方法。不知道大家发现没有, Thread 和 Runnable 都实现了 run 方法,这种操作模式其实就是代理模式。关于代理模式,我曾经写过一个小例子呵呵,大家有兴趣的话可以看一下:http://www.cnblogs.com/rollenholt/archive/2011/08/18/2144847.html + +Thread 和 Runnable 的区别: + +如果一个类继承 Thread,则不适合资源共享。但是如果实现了 Runable 接口的话,则很容易的实现资源共享。 + +``` +/** + * @author Rollen-Holt 继承Thread类,不能资源共享 + * */ +class hello extends Thread { + public void run() { + for (int i = 0; i < 7; i++) { + if (count > 0) { + System.out.println("count= " + count--); + } + } + } + + public static void main(String[] args) { + hello h1 = new hello(); + hello h2 = new hello(); + hello h3 = new hello(); + h1.start(); + h2.start(); + h3.start(); + } + + private int count = 5; +} +``` + +【运行结果】: + +count= 5 + +count= 4 + +count= 3 + +count= 2 + +count= 1 + +count= 5 + +count= 4 + +count= 3 + +count= 2 + +count= 1 + +count= 5 + +count= 4 + +count= 3 + +count= 2 + +count= 1 + +大家可以想象,如果这个是一个买票系统的话,如果 count 表示的是车票的数量的话,说明并没有实现资源的共享。 + +我们换为 Runnable 接口 + +``` +class MyThread implements Runnable{ + + private int ticket = 5; //5张票 + + public void run() { + for (int i=0; i<=20; i++) { + if (this.ticket > 0) { + System.out.println(Thread.currentThread().getName()+ "正在卖票"+this.ticket--); + } + } + } +} +public class lzwCode { + + public static void main(String [] args) { + MyThread my = new MyThread(); + new Thread(my, "1号窗口").start(); + new Thread(my, "2号窗口").start(); + new Thread(my, "3号窗口").start(); + } +} +``` + +【运行结果】: + +count= 5 + +count= 4 + +count= 3 + +count= 2 + +count= 1 + +总结一下吧: + +实现 Runnable 接口比继承 Thread 类所具有的优势: + +1):适合多个相同的程序代码的线程去处理同一个资源 + +2):可以避免 java 中的单继承的限制 + +3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。 + + + +所以,本人建议大家劲量实现接口。 + +``` +/** + * @author Rollen-Holt + * 取得线程的名称 + * */ +class hello implements Runnable { + public void run() { + for (int i = 0; i < 3; i++) { + System.out.println(Thread.currentThread().getName()); + } + } + + public static void main(String[] args) { + hello he = new hello(); + new Thread(he,"A").start(); + new Thread(he,"B").start(); + new Thread(he).start(); + } +} +``` + +【运行结果】: + +A + +A + +A + +B + +B + +B + +Thread-0 + +Thread-0 + +Thread-0 + +说明如果我们没有指定名字的话,系统自动提供名字。 + +提醒一下大家:main 方法其实也是一个线程。在 java 中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到 CPU 的资源。 + +在 java 中,每次程序运行至少启动2个线程。一个是 main 线程,一个是垃圾收集线程。因为每当使用 java 命令执行一个类的时候,实际上都会启动一个 JVM,每一个 JVM 实习在就是在操作系统中启动了一个进程。 + +判断线程是否启动 + +``` +/** + * @author Rollen-Holt 判断线程是否启动 + * */ +class hello implements Runnable { + public void run() { + for (int i = 0; i < 3; i++) { + System.out.println(Thread.currentThread().getName()); + } + } + + public static void main(String[] args) { + hello he = new hello(); + Thread demo = new Thread(he); + System.out.println("线程启动之前---》" + demo.isAlive()); + demo.start(); + System.out.println("线程启动之后---》" + demo.isAlive()); + } +} +``` + +【运行结果】 + +线程启动之前---》false + +线程启动之后---》true + +Thread-0 + +Thread-0 + +Thread-0 + +主线程也有可能在子线程结束之前结束。并且子线程不受影响,不会因为主线程的结束而结束。 + +线程的强制执行: + +``` +/** + * @author Rollen-Holt 线程的强制执行 + * */ + class hello implements Runnable { + public void run() { + for (int i = 0; i < 3; i++) { + System.out.println(Thread.currentThread().getName()); + } + } + + public static void main(String[] args) { + hello he = new hello(); + Thread demo = new Thread(he,"线程"); + demo.start(); + for(int i=0;i<50;++i){ + if(i>10){ + try{ + demo.join(); //强制执行demo + }catch (Exception e) { + e.printStackTrace(); + } + } + System.out.println("main 线程执行-->"+i); + } + } + } +``` + +【运行的结果】: + +main 线程执行-->0 + +main 线程执行-->1 + +main 线程执行-->2 + +main 线程执行-->3 + +main 线程执行-->4 + +main 线程执行-->5 + +main 线程执行-->6 + +main 线程执行-->7 + +main 线程执行-->8 + +main 线程执行-->9 + +main 线程执行-->10 + +线程 + +线程 + +线程 + +main 线程执行-->11 + +main 线程执行-->12 + +main 线程执行-->13 + +... + + + +线程的休眠: + +``` +/** + * @author Rollen-Holt 线程的休眠 + * */ +class hello implements Runnable { + public void run() { + for (int i = 0; i < 3; i++) { + try { + Thread.sleep(2000); + } catch (Exception e) { + e.printStackTrace(); + } + System.out.println(Thread.currentThread().getName() + i); + } + } + + public static void main(String[] args) { + hello he = new hello(); + Thread demo = new Thread(he, "线程"); + demo.start(); + } +} +``` + +【运行结果】:(结果每隔 2 s 输出一个) + +线程0 + +线程1 + +线程2 + + + +线程的中断: + +``` +/** + * @author Rollen-Holt 线程的中断 + * */ +class hello implements Runnable { + public void run() { + System.out.println("执行run方法"); + try { + Thread.sleep(10000); + System.out.println("线程完成休眠"); + } catch (Exception e) { + System.out.println("休眠被打断"); + return; //返回到程序的调用处 + } + System.out.println("线程正常终止"); + } + + public static void main(String[] args) { + hello he = new hello(); + Thread demo = new Thread(he, "线程"); + demo.start(); + try{ + Thread.sleep(2000); + }catch (Exception e) { + e.printStackTrace(); + } + demo.interrupt(); //2s后中断线程 + } +} +``` + +【运行结果】: + +执行 run 方法 + +休眠被打断 + +在 java 程序中,只要前台有一个线程在运行,整个 java 程序进程不会小时,所以此时可以设置一个后台线程,这样即使 java 进程小时了,此后台线程依然能够继续运行。 + +``` +/** + * @author Rollen-Holt 后台线程 + * */ +class hello implements Runnable { + public void run() { + while (true) { + System.out.println(Thread.currentThread().getName() + "在运行"); + } + } + + public static void main(String[] args) { + hello he = new hello(); + Thread demo = new Thread(he, "线程"); + demo.setDaemon(true); + demo.start(); + } +} +``` + +虽然有一个死循环,但是程序还是可以执行完的。因为在死循环中的线程操作已经设置为后台运行了。 + +线程的优先级: + +``` +/** + * @author Rollen-Holt 线程的优先级 + * */ +class hello implements Runnable { + public void run() { + for(int i=0;i<5;++i){ + System.out.println(Thread.currentThread().getName()+"运行"+i); + } + } + + public static void main(String[] args) { + Thread h1=new Thread(new hello(),"A"); + Thread h2=new Thread(new hello(),"B"); + Thread h3=new Thread(new hello(),"C"); + h1.setPriority(8); + h2.setPriority(2); + h3.setPriority(6); + h1.start(); + h2.start(); + h3.start(); + + } +} +``` + +【运行结果】: + +A 运行0 + +A 运行1 + +A 运行2 + +A 运行3 + +A 运行4 + +B 运行0 + +C 运行0 + +C 运行1 + +C 运行2 + +C 运行3 + +C 运行4 + +B 运行1 + +B 运行2 + +B 运行3 + +B 运行4 + +但是请读者不要误以为优先级越高就先执行。谁先执行还是取决于谁先去的 CPU 的资源、 + +另外,主线程的优先级是5. + +线程的礼让。 + +在线程操作中,也可以使用 yield()方法,将一个线程的操作暂时交给其他线程执行。 + +``` +/** + * @author Rollen-Holt 线程的优先级 + * */ +class hello implements Runnable { + public void run() { + for(int i=0;i<5;++i){ + System.out.println(Thread.currentThread().getName()+"运行"+i); + if(i==3){ + System.out.println("线程的礼让"); + Thread.currentThread().yield(); + } + } + } + + public static void main(String[] args) { + Thread h1=new Thread(new hello(),"A"); + Thread h2=new Thread(new hello(),"B"); + h1.start(); + h2.start(); + + } +} +``` + +A 运行0 + +A 运行1 + +A 运行2 + +A 运行3 + +线程的礼让 + +A 运行4 + +B 运行0 + +B 运行1 + +B 运行2 + +B 运行3 + +线程的礼让 + +B 运行4 + +同步和死锁: + +【问题引出】:比如说对于买票系统,有下面的代码: + +``` +/** + * @author Rollen-Holt + * */ +class hello implements Runnable { + public void run() { + for(int i=0;i<10;++i){ + if(count>0){ + try{ + Thread.sleep(1000); + }catch(InterruptedException e){ + e.printStackTrace(); + } + System.out.println(count--); + } + } + } + + public static void main(String[] args) { + hello he=new hello(); + Thread h1=new Thread(he); + Thread h2=new Thread(he); + Thread h3=new Thread(he); + h1.start(); + h2.start(); + h3.start(); + } + private int count=5; +} +``` + +【运行结果】: + +5 + +4 + +3 + +2 + +1 + +0 + +-1 + +这里出现了-1,显然这个是错的。,应该票数不能为负值。 + +如果想解决这种问题,就需要使用同步。所谓同步就是在统一时间段中只有有一个线程运行, + +其他的线程必须等到这个线程结束之后才能继续执行。 + +【使用线程同步解决问题】 + +采用同步的话,可以使用同步代码块和同步方法两种来完成。 + +【同步代码块】: + +语法格式: + +``` + +synchronized(同步对象){ + + //需要同步的代码 + +} +``` + +但是一般都把当前对象 this 作为同步对象。 + +比如对于上面的买票的问题,如下: + +``` +/** + * @author Rollen-Holt + * */ +class hello implements Runnable { + public void run() { + for(int i=0;i<10;++i){ + synchronized (this) { + if(count>0){ + try{ + Thread.sleep(1000); + }catch(InterruptedException e){ + e.printStackTrace(); + } + System.out.println(count--); + } + } + } + } + + public static void main(String[] args) { + hello he=new hello(); + Thread h1=new Thread(he); + Thread h2=new Thread(he); + Thread h3=new Thread(he); + h1.start(); + h2.start(); + h3.start(); + } + private int count=5; +} +``` + +【运行结果】:(每一秒输出一个结果) + +5 + +4 + +3 + +2 + +1 + +【同步方法】 + +也可以采用同步方法。 + +语法格式为 + +``` +synchronized 方法返回类型方法名(参数列表){ + + // 其他代码 + +} +``` + +现在,我们采用同步方法解决上面的问题。 + +``` +/** + * @author Rollen-Holt + * */ +class hello implements Runnable { + public void run() { + for (int i = 0; i < 10; ++i) { + sale(); + } + } + + public synchronized void sale() { + if (count > 0) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(count--); + } + } + + public static void main(String[] args) { + hello he = new hello(); + Thread h1 = new Thread(he); + Thread h2 = new Thread(he); + Thread h3 = new Thread(he); + h1.start(); + h2.start(); + h3.start(); + } + + private int count = 5; +} +``` + +【运行结果】(每秒输出一个) + +5 + +4 + +3 + +2 + +1 + +提醒一下,当多个线程共享一个资源的时候需要进行同步,但是过多的同步可能导致死锁。 + +此处列举经典的生产者和消费者问题。 + +【生产者和消费者问题】 + +先看一段有问题的代码。 + +``` +class Info { + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + private String name = "Rollen"; + private int age = 20; +} + +/** + * 生产者 + * */ +class Producer implements Runnable{ + private Info info=null; + Producer(Info info){ + this.info=info; + } + + public void run(){ + boolean flag=false; + for(int i=0;i<25;++i){ + if(flag){ + this.info.setName("Rollen"); + try{ + Thread.sleep(100); + }catch (Exception e) { + e.printStackTrace(); + } + this.info.setAge(20); + flag=false; + }else{ + this.info.setName("chunGe"); + try{ + Thread.sleep(100); + }catch (Exception e) { + e.printStackTrace(); + } + this.info.setAge(100); + flag=true; + } + } + } +} +/** + * 消费者类 + * */ +class Consumer implements Runnable{ + private Info info=null; + public Consumer(Info info){ + this.info=info; + } + + public void run(){ + for(int i=0;i<25;++i){ + try{ + Thread.sleep(100); + }catch (Exception e) { + e.printStackTrace(); + } + System.out.println(this.info.getName()+"<---->"+this.info.getAge()); + } + } +} + +/** + * 测试类 + * */ +class hello{ + public static void main(String[] args) { + Info info=new Info(); + Producer pro=new Producer(info); + Consumer con=new Consumer(info); + new Thread(pro).start(); + new Thread(con).start(); + } +} +``` + +【运行结果】: + +Rollen<---->100 + +chunGe<---->20 + +chunGe<---->100 + +Rollen<---->100 + +chunGe<---->20 + +Rollen<---->100 + +Rollen<---->100 + +Rollen<---->100 + +chunGe<---->20 + +chunGe<---->20 + +chunGe<---->20 + +Rollen<---->100 + +chunGe<---->20 + +Rollen<---->100 + +chunGe<---->20 + +Rollen<---->100 + +chunGe<---->20 + +Rollen<---->100 + +chunGe<---->20 + +Rollen<---->100 + +chunGe<---->20 + +Rollen<---->100 + +chunGe<---->20 + +Rollen<---->100 + +chunGe<---->20 + +大家可以从结果中看到,名字和年龄并没有对于。 + +那么如何解决呢? + +1)加入同步 + +2)加入等待和唤醒 + +先来看看加入同步会是如何。 + +``` +class Info { + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public synchronized void set(String name, int age){ + this.name=name; + try{ + Thread.sleep(100); + }catch (Exception e) { + e.printStackTrace(); + } + this.age=age; + } + + public synchronized void get(){ + try{ + Thread.sleep(100); + }catch (Exception e) { + e.printStackTrace(); + } + System.out.println(this.getName()+"<===>"+this.getAge()); + } + private String name = "Rollen"; + private int age = 20; +} + +/** + * 生产者 + * */ +class Producer implements Runnable { + private Info info = null; + + Producer(Info info) { + this.info = info; + } + + public void run() { + boolean flag = false; + for (int i = 0; i < 25; ++i) { + if (flag) { + + this.info.set("Rollen", 20); + flag = false; + } else { + this.info.set("ChunGe", 100); + flag = true; + } + } + } +} + +/** + * 消费者类 + * */ +class Consumer implements Runnable { + private Info info = null; + + public Consumer(Info info) { + this.info = info; + } + + public void run() { + for (int i = 0; i < 25; ++i) { + try { + Thread.sleep(100); + } catch (Exception e) { + e.printStackTrace(); + } + this.info.get(); + } + } +} + +/** + * 测试类 + * */ +class hello { + public static void main(String[] args) { + Info info = new Info(); + Producer pro = new Producer(info); + Consumer con = new Consumer(info); + new Thread(pro).start(); + new Thread(con).start(); + } +} +``` + +【运行结果】: + +Rollen<===>20 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +Rollen<===>20 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +ChunGe<===>100 + +从运行结果来看,错乱的问题解决了,现在是 Rollen 对应20,ChunGe 对于100,但是还是出现了重复读取的问题,也肯定有重复覆盖的问题。如果想解决这个问题,就需要使用 Object 类帮忙了,我们可以使用其中的等待和唤醒操作。 + +要完成上面的功能,我们只需要修改 Info 类饥渴,在其中加上标志位,并且通过判断标志位完成等待和唤醒的操作,代码如下: + +``` +class Info { + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public synchronized void set(String name, int age){ + if(!flag){ + try{ + super.wait(); + }catch (Exception e) { + e.printStackTrace(); + } + } + this.name=name; + try{ + Thread.sleep(100); + }catch (Exception e) { + e.printStackTrace(); + } + this.age=age; + flag=false; + super.notify(); + } + + public synchronized void get(){ + if(flag){ + try{ + super.wait(); + }catch (Exception e) { + e.printStackTrace(); + } + } + + try{ + Thread.sleep(100); + }catch (Exception e) { + e.printStackTrace(); + } + System.out.println(this.getName()+"<===>"+this.getAge()); + flag=true; + super.notify(); + } + private String name = "Rollen"; + private int age = 20; + private boolean flag=false; +} + +/** + * 生产者 + * */ +class Producer implements Runnable { + private Info info = null; + + Producer(Info info) { + this.info = info; + } + + public void run() { + boolean flag = false; + for (int i = 0; i < 25; ++i) { + if (flag) { + + this.info.set("Rollen", 20); + flag = false; + } else { + this.info.set("ChunGe", 100); + flag = true; + } + } + } +} + +/** + * 消费者类 + * */ +class Consumer implements Runnable { + private Info info = null; + + public Consumer(Info info) { + this.info = info; + } + + public void run() { + for (int i = 0; i < 25; ++i) { + try { + Thread.sleep(100); + } catch (Exception e) { + e.printStackTrace(); + } + this.info.get(); + } + } +} + +/** + * 测试类 + * */ +class hello { + public static void main(String[] args) { + Info info = new Info(); + Producer pro = new Producer(info); + Consumer con = new Consumer(info); + new Thread(pro).start(); + new Thread(con).start(); + } +} +``` + +【程序运行结果】: +Rollen<===>20 +ChunGe<===>100 +Rollen<===>20 +ChunGe<===>100 +Rollen<===>20 +ChunGe<===>100 +Rollen<===>20 +ChunGe<===>100 +Rollen<===>20 +ChunGe<===>100 +Rollen<===>20 +ChunGe<===>100 +Rollen<===>20 +ChunGe<===>100 +Rollen<===>20 +ChunGe<===>100 +Rollen<===>20 +ChunGe<===>100 +Rollen<===>20 +ChunGe<===>100 +Rollen<===>20 +ChunGe<===>100 +Rollen<===>20 +ChunGe<===>100 +Rollen<===>20 +先在看结果就可以知道,之前的问题完全解决。 +《完》 + +PS(写在后面): + +本人深知学的太差,所以希望大家能多多指点。另外,关于多线程其实有很多的知识,由于目前我也就知道的不太多,写了一些常用的。虽然在操作系统这门课上学了很多的线程和进程,比如银行家算法等等的,以后有时间在补充,大家有什么好资料可以留个言,大家一起分享一下,谢谢了 \ No newline at end of file diff --git a/netty.md b/netty.md new file mode 100644 index 0000000..18f04a5 --- /dev/null +++ b/netty.md @@ -0,0 +1,352 @@ +# Netty + +# Netty 系列之 Netty 高性能之道 + +# 1. 背景 + +## 1.1. 惊人的性能数据 + +最近一个圈内朋友通过私信告诉我,通过使用 Netty4 + Thrift 压缩二进制编解码技术,他们实现了 10 W TPS(1 K 的复杂 POJO 对象)的跨节点远程服务调用。相比于传统基于 Java 序列化 +BIO(同步阻塞 IO)的通信框架,性能提升了8倍多。 + +事实上,我对这个数据并不感到惊讶,根据我5年多的 NIO 编程经验,通过选择合适的 NIO 框架,加上高性能的压缩二进制编解码技术,精心的设计 Reactor 线程模型,达到上述性能指标是完全有可能的。 + +下面我们就一起来看下 Netty 是如何支持 10 W TPS的跨节点远程服务调用的,在正式开始讲解之前,我们先简单介绍下 Netty。 + +## 1.2. Netty 基础入门 + +Netty 是一个高性能、异步事件驱动的 NIO 框架,它提供了对 TCP、UDP 和文件传输的支持,作为一个异步 NIO 框架,Netty 的所有 IO 操作都是异步非阻塞的,通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果。 + +作为当前最流行的 NIO 框架,Netty 在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于 Netty 的 NIO 框架构建。 + +# 2. Netty 高性能之道 + +## 2.1. RPC 调用的性能模型分析 + +### 2.1.1. 传统 RPC 调用性能差的三宗罪 + +网络传输方式问题:传统的 RPC 框架或者基于 RMI 等方式的远程服务(过程)调用采用了同步阻塞 IO,当客户端的并发压力或者网络时延增大之后,同步阻塞 IO 会由于频繁的 wait 导致 IO 线程经常性的阻塞,由于线程无法高效的工作,IO 处理能力自然下降。 + +下面,我们通过 BIO 通信模型图看下 BIO 通信的弊端: + +![](images/37.png) + +图2-1 BIO 通信模型图 + +采用 BIO 通信模型的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接,接收到客户端连接之后为客户端连接创建一个新的线程处理请求消息,处理完成之后,返回应答消息给客户端,线程销毁,这就是典型的一请求一应答模型。该架构最大的问题就是不具备弹性伸缩能力,当并发访问量增加后,服务端的线程个数和并发访问数成线性正比,由于线程是 JAVA 虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能急剧下降,随着并发量的继续增加,可能会发生句柄溢出、线程堆栈溢出等问题,并导致服务器最终宕机。 + +序列化方式问题:Java 序列化存在如下几个典型问题: + +1) Java 序列化机制是 Java 内部的一种对象编解码技术,无法跨语言使用;例如对于异构系统之间的对接,Java 序列化后的码流需要能够通过其它语言反序列化成原始对象(副本),目前很难支持; + +2) 相比于其它开源的序列化框架,Java 序列化后的码流太大,无论是网络传输还是持久化到磁盘,都会导致额外的资源占用; + +3) 序列化性能差(CPU 资源占用高)。 + +线程模型问题:由于采用同步阻塞 IO,这会导致每个 TCP 连接都占用1个线程,由于线程资源是 JVM 虚拟机非常宝贵的资源,当 IO 读写阻塞导致线程无法及时释放时,会导致系统性能急剧下降,严重的甚至会导致虚拟机无法创建新的线程。 + +### 2.1.2. 高性能的三个主题 +1) 传输:用什么样的通道将数据发送给对方,BIO、NIO 或者 AIO,IO 模型在很大程度上决定了框架的性能。 + +2) 协议:采用什么样的通信协议,HTTP 或者内部私有协议。协议的选择不同,性能模型也不同。相比于公有协议,内部私有协议的性能通常可以被设计的更优。 + +3) 线程:数据报如何读取?读取之后的编解码在哪个线程进行,编解码后的消息如何派发, Reactor 线程模型的不同,对性能的影响也非常大。 + +![](images/38.png) + +图2-2 RPC 调用性能三要素 + +## 2.2. Netty 高性能之道 + +### 2.2.1. 异步非阻塞通信 +在 IO 编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者 IO 多路复用技术进行处理。IO 多路复用技术通过把多个 IO 的阻塞复用到同一个 select 的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O 多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。 + +JDK1.4 提供了对非阻塞 IO(NIO)的支持,JDK1.5_update10 版本使用 epoll 替代了传统的 select/poll,极大的提升了 NIO 通信的性能。 + +JDK NIO 通信模型如下所示: + +![](images/39.png) + +图2-3 NIO 的多路复用模型图 + +与 Socket 类和 ServerSocket 类相对应,NIO 也提供了 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式正好相反。开发人员一般可以根据自己的需要来选择合适的模式,一般来说,低负载、低并发的应用程序可以选择同步阻塞 IO 以降低编程复杂度。但是对于高负载、高并发的网络应用,需要使用 NIO 的非阻塞模式进行开发。 + +Netty 架构按照 Reactor 模式设计和实现,它的服务端通信序列图如下: + +![](images/40.png) + +图2-3 NIO 服务端通信序列图 + +客户端通信序列图如下: + +![](images/41.png) + +图2-4 NIO 客户端通信序列图 + +Netty 的 IO 线程 NioEventLoop 由于聚合了多路复用器 Selector,可以同时并发处理成百上千个客户端 Channel,由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 IO 阻塞导致的线程挂起。另外,由于 Netty 采用了异步通信模式,一个 IO 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 IO 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。 + +### 2.2.2. 零拷贝 +很多用户都听说过 Netty 具有“零拷贝”功能,但是具体体现在哪里又说不清楚,本小节就详细对 Netty 的“零拷贝”功能进行讲解。 + +Netty 的“零拷贝”主要体现在如下三个方面: + +1) Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。 + +2) Netty 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer。 + +3) Netty 的文件传输采用了 transferTo 方法,它可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。 + +下面,我们对上述三种“零拷贝”进行说明,先看 Netty 接收 Buffer 的创建: + +![](images/42.png) + +图2-5 异步消息读取“零拷贝” + +每循环读取一次消息,就通过 ByteBufAllocator的ioBuffer 方法获取 ByteBuf 对象,下面继续看它的接口定义: + +![](images/43.png) + +图2-6 ByteBufAllocator 通过 ioBuffer 分配堆外内存 + +当进行 Socket IO 读写的时候,为了避免从堆内存拷贝一份副本到直接内存,Netty 的 ByteBuf 分配器直接创建非堆内存避免缓冲区的二次拷贝,通过“零拷贝”来提升读写性能。 + +下面我们继续看第二种“零拷贝”的实现 CompositeByteBuf,它对外将多个 ByteBuf 封装成一个 ByteBuf,对外提供统一封装后的 ByteBuf 接口,它的类定义如下: + +![](images/44.png) + +图2-7 CompositeByteBuf 类继承关系 + +通过继承关系我们可以看出 CompositeByteBuf 实际就是个 ByteBuf 的包装器,它将多个 ByteBuf 组合成一个集合,然后对外提供统一的 ByteBuf 接口,相关定义如下: + +![](images/45.png) + +图2-8 CompositeByteBuf 类定义 + +添加 ByteBuf,不需要做内存拷贝,相关代码如下: + +![](images/46.png) + +图2-9 新增 ByteBuf 的“零拷贝” + +最后,我们看下文件传输的“零拷贝”: + +![](images/47.png) + +图2-10 文件传输“零拷贝” + +Netty 文件传输 DefaultFileRegion 通过 transferTo 方法将文件发送到目标 Channel 中,下面重点看 FileChannel 的 transferTo 方法,它的 API DOC 说明如下: + +![](images/48.png) + +图2-11 文件传输 “零拷贝” + +对于很多操作系统它直接将文件缓冲区的内容发送到目标 Channel 中,而不需要通过拷贝的方式,这是一种更加高效的传输方式,它实现了文件传输的“零拷贝”。 + +### 2.2.3. 内存池 +随着 JVM 虚拟机和 JIT 即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。但是对于缓冲区 Buffer,情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,Netty 提供了基于内存池的缓冲区重用机制。下面我们一起看下 Netty ByteBuf 的实现: + +![](images/49.png) + +图2-12 内存池 ByteBuf + +Netty 提供了多种内存管理策略,通过在启动辅助类中配置相关参数,可以实现差异化的定制。 + +下面通过性能测试,我们看下基于内存池循环利用的 ByteBuf 和普通 ByteBuf 的性能差异。 + +用例一,使用内存池分配器创建直接内存缓冲区: + +![](images/50.png) + +图2-13 基于内存池的非堆内存缓冲区测试用例 + +用例二,使用非堆内存分配器创建的直接内存缓冲区: + +![](images/51.png) + +图2-14 基于非内存池创建的非堆内存缓冲区测试用例 + +各执行300万次,性能对比结果如下所示: + +![](images/52.png) + +图2-15 内存池和非内存池缓冲区写入性能对比 + +性能测试表明,采用内存池的 ByteBuf 相比于朝生夕灭的 ByteBuf,性能高23倍左右(性能数据与使用场景强相关)。 + +下面我们一起简单分析下 Netty 内存池的内存分配: + +![](images/53.png) + +图2-16 AbstractByteBufAllocator 的缓冲区分配 + +继续看 newDirectBuffer 方法,我们发现它是一个抽象方法,由 AbstractByteBufAllocator 的子类负责具体实现,代码如下: + +![](images/54.png) + +图2-17 newDirectBuffer 的不同实现 + +代码跳转到 PooledByteBufAllocator 的 newDirectBuffer 方法,从 Cache 中获取内存区域 PoolArena,调用它的 allocate 方法进行内存分配: + +![](images/55.png) + +图2-18 PooledByteBufAllocator 的内存分配 + +PoolArena 的 allocate 方法如下: + +![](images/56.png) + +图2-18 PoolArena 的缓冲区分配 + +我们重点分析 newByteBuf 的实现,它同样是个抽象方法,由子类 DirectArena 和 HeapArena 来实现不同类型的缓冲区分配,由于测试用例使用的是堆外内存, + +![](images/57.png) + +图2-19 PoolArena 的 newByteBuf 抽象方法 + +因此重点分析 DirectArena 的实现:如果没有开启使用 sun 的 unsafe,则 + +![](images/58.png) + +图2-20 DirectArena 的 newByteBuf 方法实现 + +执行 PooledDirectByteBuf 的 newInstance 方法,代码如下: + +![](images/59.png) + +图2-21 PooledDirectByteBuf 的 newInstance 方法实现 + +通过 RECYCLER 的 get 方法循环使用 ByteBuf 对象,如果是非内存池实现,则直接创建一个新的 ByteBuf 对象。从缓冲池中获取 ByteBuf 之后,调用 AbstractReferenceCountedByteBuf的setRefCnt 方法设置引用计数器,用于对象的引用计数和内存回收(类似 JVM 垃圾回收机制)。 + +### 2.2.4. 高效的 Reactor 线程模型 +常用的 Reactor 线程模型有三种,分别如下: + +1) Reactor 单线程模型; + +2) Reactor 多线程模型; + +3) 主从 Reactor 多线程模型 + +Reactor 单线程模型,指的是所有的 IO 操作都在同一个 NIO 线程上面完成,NIO 线程的职责如下: + +1) 作为 NIO 服务端,接收客户端的 TCP 连接; + +2) 作为 NIO 客户端,向服务端发起 TCP 连接; + +3) 读取通信对端的请求或者应答消息; + +4) 向通信对端发送消息请求或者应答消息。 + +Reactor 单线程模型示意图如下所示: + +![](images/60.png) + +图2-22 Reactor 单线程模型 + +由于 Reactor 模式使用的是异步非阻塞 IO,所有的 IO 操作都不会导致阻塞,理论上一个线程可以独立处理所有 IO 相关的操作。从架构层面看,一个 NIO 线程确实可以完成其承担的职责。例如,通过 Acceptor 接收客户端的 TCP 连接请求消息,链路建立成功之后,通过 Dispatch 将对应的 ByteBuffer 派发到指定的 Handler 上进行消息解码。用户 Handler 可以通过 NIO 线程将消息发送给客户端。 + +对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发的应用却不合适,主要原因如下: + +1) 一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,即便 NIO 线程的 CPU 负荷达到100%,也无法满足海量消息的编码、解码、读取和发送; + +2) 当 NIO 线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了 NIO 线程的负载,最终会导致大量消息积压和处理超时,NIO 线程会成为系统的性能瓶颈; + +3) 可靠性问题:一旦 NIO 线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。 + +为了解决这些问题,演进出了 Reactor 多线程模型,下面我们一起学习下 Reactor 多线程模型。 + +Rector 多线程模型与单线程模型最大的区别就是有一组 NIO 线程处理 IO 操作,它的原理图如下: + +![](images/61.png) + +图2-23 Reactor 多线程模型 + +Reactor 多线程模型的特点: + +1) 有专门一个 NIO 线程-Acceptor 线程用于监听服务端,接收客户端的 TCP 连接请求; + +2) 网络 IO 操作-读、写等由一个 NIO 线程池负责,线程池可以采用标准的 JDK 线程池实现,它包含一个任务队列和 N 个可用的线程,由这些 NIO 线程负责消息的读取、解码、编码和发送; + +3) 1个 NIO 线程可以同时处理 N 条链路,但是1个链路只对应1个 NIO 线程,防止发生并发操作问题。 + +在绝大多数场景下,Reactor 多线程模型都可以满足性能需求;但是,在极特殊应用场景中,一个 NIO 线程负责监听和处理所有的客户端连接可能会存在性能问题。例如百万客户端并发连接,或者服务端需要对客户端的握手消息进行安全认证,认证本身非常损耗性能。在这类场景下,单独一个 Acceptor 线程可能会存在性能不足问题,为了解决性能问题,产生了第三种 Reactor 线程模型-主从 Reactor 多线程模型。 + +主从 Reactor 线程模型的特点是:服务端用于接收客户端连接的不再是个1个单独的 NIO 线程,而是一个独立的 NIO 线程池。Acceptor 接收到客户端 TCP 连接请求处理完成后(可能包含接入认证等),将新创建的 SocketChannel 注册到 IO 线程池(sub reactor 线程池)的某个 IO 线程上,由它负责 SocketChannel 的读写和编解码工作。Acceptor 线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到后端 subReactor 线程池的 IO 线程上,由 IO 线程负责后续的 IO 操作。 + +它的线程模型如下图所示: + +![](images/62.png) + +图2-24 Reactor 主从多线程模型 + +利用主从 NIO 线程模型,可以解决1个服务端监听线程无法有效处理所有客户端连接的性能不足问题。因此,在 Netty 的官方 demo 中,推荐使用该线程模型。 + +事实上,Netty 的线程模型并非固定不变,通过在启动辅助类中创建不同的 EventLoopGroup 实例并通过适当的参数配置,就可以支持上述三种 Reactor 线程模型。正是因为 Netty 对 Reactor 线程模型的支持提供了灵活的定制能力,所以可以满足不同业务场景的性能诉求。 + +### 2.2.5. 无锁化的串行设计理念 +在大多数场景下,并行多线程处理可以提升系统的并发性能。但是,如果对于共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会导致性能的下降。为了尽可能的避免锁竞争带来的性能损耗,可以通过串行化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。 + +为了尽可能提升性能,Netty 采用了串行无锁化设计,在 IO 线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。 + +Netty 的串行化设计工作原理图如下: + +![](images/63.png) + +图2-25 Netty 串行化工作原理图 + +Netty 的 NioEventLoop 读取到消息之后,直接调用 ChannelPipeline 的 fireChannelRead(Object msg),只要用户不主动切换线程,一直会由 NioEventLoop 调用到用户的 Handler,期间不进行线程切换,这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优的。 + +### 2.2.6. 高效的并发编程 +Netty 的高效并发编程主要体现在如下几点: + +1) volatile 的大量、正确使用; + +2) CAS 和原子类的广泛使用; + +3) 线程安全容器的使用; + +4) 通过读写锁提升并发性能。 + +如果大家想了解 Netty 高效并发编程的细节,可以阅读之前我在微博分享的《多线程并发编程在 Netty 中的应用分析》,在这篇文章中对 Netty 的多线程技巧和应用进行了详细的介绍和分析。 + +### 2.2.7. 高性能的序列化框架 +影响序列化性能的关键因素总结如下: + +1) 序列化后的码流大小(网络带宽的占用); + +2) 序列化&反序列化的性能(CPU 资源占用); + +3) 是否支持跨语言(异构系统的对接和开发语言切换)。 + +Netty 默认提供了对 Google Protobuf 的支持,通过扩展 Netty 的编解码接口,用户可以实现其它的高性能序列化框架,例如 Thrift 的压缩二进制编解码框架。 + +下面我们一起看下不同序列化&反序列化框架序列化后的字节数组对比: + +![](images/64.png) + +图2-26 各序列化框架序列化码流大小对比 + +从上图可以看出,Protobuf 序列化后的码流只有 Java 序列化的1/4左右。正是由于 Java 原生序列化性能表现太差,才催生出了各种高性能的开源序列化技术和框架(性能差只是其中的一个原因,还有跨语言、IDL 定义等其它因素)。 + +### 2.2.8. 灵活的 TCP 参数配置能力 +合理设置 TCP 参数在某些场景下对于性能的提升可以起到显著的效果,例如 SO_RCVBUF 和 SO_SNDBUF。如果设置不当,对性能的影响是非常大的。下面我们总结下对性能影响比较大的几个配置项: + +1) SO_RCVBUF 和 SO_SNDBUF:通常建议值为 128 K 或者 256 K; + +2) SO_TCPNODELAY:NAGLE 算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法; + +3) 软中断:如果 Linux 内核版本支持 RPS(2.6.35以上版本),开启 RPS 后可以实现软中断,提升网络吞吐量。RPS 根据数据包的源地址,目的地址以及目的和源端口,计算出一个 hash值,然后根据这个 hash 值来选择软中断运行的 cpu,从上层来看,也就是说将每个连接和 cpu 绑定,并通过这个 hash 值,来均衡软中断在多个 cpu 上,提升网络并行处理性能。 + +Netty 在启动辅助类中可以灵活的配置 TCP 参数,满足不同的用户场景。相关配置接口定义如下: + +![](images/65.png) + +图2-27 Netty 的 TCP 参数配置定义 + +2.3. 总结 + +通过对 Netty 的架构和性能模型进行分析,我们发现 Netty 架构的高性能是被精心设计和实现的,得益于高质量的架构和代码,Netty 支持 10W TPS 的跨节点服务调用并不是件十分困难的事情。 + +3. 作者简介 + +李林锋,2007年毕业于东北大学,2008年进入华为公司从事高性能通信软件的设计和开发工作,有6年 NIO 设计和开发经验,精通 Netty、Mina 等 NIO 框架。Netty 中国社区创始人,《Netty 权威指南》作者。 + +联系方式:新浪微博 Nettying 微信:Nettying \ No newline at end of file diff --git a/platorm-memory.md b/platorm-memory.md new file mode 100644 index 0000000..ff217cb --- /dev/null +++ b/platorm-memory.md @@ -0,0 +1,372 @@ +# Java 内存管理 + +# java 内存管理机制 + +JAVA 内存管理总结 + +1. java 是如何管理内存的 + + Java 的内存管理就是对象的分配和释放问题。(两部分) + +分配 :内存的分配是由程序完成的,程序员需要通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。 +释放 :对象的释放是由垃圾回收机制决定和执行的,这样做确实简化了程序员的工作。但同时,它也加重了 JVM 的工作。因为,GC 为了能够正确释放对象,GC 必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC 都需要进行监控。 + +2. 什么叫 java 的内存泄露 + +在 Java 中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连(也就是说仍存在该内存对象的引用);其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为 Java 中的内存泄漏,这些对象不会被 GC 所回收,然而它却占用内存。 + +3. JVM 的内存区域组成 + +java 把内存分两种:一种是栈内存,另一种是堆内存1。在函数中定义的基本类型变量和对象的引用变量都在函数的栈内存中分配;2。堆内存用来存放由 new 创建的对象和数组以及对象的实例变量 在函数(代码块)中定义一个变量时,java 就在栈中为这个变量分配内存空间,当超过变量的作用域后,java 会自动释放掉为该变量所分配的内存空间;在堆中分配的内存由java 虚拟机的自动垃圾回收器来管理 + +堆和栈的优缺点 + +堆的优势是可以动态分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的。 + +缺点就是要在运行时动态分配内存,存取速度较慢; 栈的优势是,存取速度比堆要快,仅次于直接位于 CPU 中的寄存器。 + +另外,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。 + +4. Java 中数据在内存中是如何存储的 + +a) 基本数据类型 + +Java 的基本数据类型共有8种,即 int, short, long, byte, float, double, boolean, char(注意,并没有 string 的基本类型)。这种类型的定义是通过诸如 int a = 3; long b = 255L;的形式来定义的。如 int a = 3;这里的 a 是一个指向 int 类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。 + +另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。比如:我们同时定义: + +``` +int a=3; +int b=3; +``` + +编译器先处理 int a = 3;首先它会在栈中创建一个变量为 a 的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将 a 指向3的地址。接着处理 int b = 3;在创建完 b 这个引用变量后,由于在栈中已经有3这个字面值,便将 b 直接指向3的地址。这样,就出现了 a 与 b 同时均指向3的情况。 定义完 a 与 b 的值后,再令 a = 4;那么,b 不会等于4,还是等于3。在编译器内部,遇到时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将 a 指向这个地址。因此 a 值的改变不会影响到 b 的值。 + +b) 对象 + +在 Java 中,创建一个对象包括对象的声明和实例化两步,下面用一个例题来说明对象的内存模型。  假设有类 Rectangle 定义如下: + +``` +public class Rectangle { +double width; +double height; +public Rectangle(double w,double h){ +w = width; +h = height; +} +} +``` + +(1)声明对象时的内存模型 + 用 Rectangle rect;声明一个对象 rect 时,将在栈内存为对象的引用变量 rect 分配内存空间,但 Rectangle 的值为空,称 rect 是一个空对象。空对象不能使用,因为它还没有引用任何"实体"。 +(2)对象实例化时的内存模型 + 当执行 rect=new Rectangle(3,5);时,会做两件事: 在堆内存中为类的成员变量width,height 分配内存,并将其初始化为各数据类型的默认值;接着进行显式初始化(类定义时的初始化值);最后调用构造方法,为成员变量赋值。 返回堆内存中对象的引用(相当于首地址)给引用变量 rect,以后就可以通过 rect 来引用堆内存中的对象了。 + +c) 创建多个不同的对象实例 + +一个类通过使用new运算符可以创建多个不同的对象实例,这些对象实例将在堆中被分配不同的内存空间,改变其中一个对象的状态不会影响其他对象的状态。例如: + +``` +Rectangle r1= new Rectangle(3,5); +Rectangle r2= new Rectangle(4,6); +``` + +此时,将在堆内存中分别为两个对象的成员变量 width、height 分配内存空间,两个对象在堆内存中占据的空间是互不相同的。如果有: + +``` +Rectangle r1= new Rectangle(3,5); +Rectangle r2=r1; +``` + +则在堆内存中只创建了一个对象实例,在栈内存中创建了两个对象引用,两个对象引用同时指向一个对象实例。 + +d) 包装类 + +基本型别都有对应的包装类:如 int 对应 Integer 类,double 对应 Double 类等,基本类型的定义都是直接在栈中,如果用包装类来创建对象,就和普通对象一样了。例如:int i=0;i 直接存储在栈中。 Integer i(i 此时是对象) = new Integer(5);这样,i 对象数据存储在堆中,i 的引用存储在栈中,通过栈中的引用来操作对象。 + +e) String + +String 是一个特殊的包装类数据。可以用用以下两种方式创建: + +``` +String str = new String("abc"); +String str = "abc"; +``` + +第一种创建方式,和普通对象的的创建过程一样; +第二种创建方式,Java 内部将此语句转化为以下几个步骤: +(1) 先定义一个名为 str 的对 String 类的对象引用变量:String str; +(2) 在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc" +地址,接着创建一个新的 String 类的对象 o,并将 o 的字符串值指向这个地址,而且在栈 +这个地址旁边记下这个引用的对象 o。如果已经有了值为"abc"的地址,则查找对象 o,并 +回 o 的地址。 +(3) 将 str 指向对象 o 的地址。 +值得注意的是,一般 String 类中字符串值都是直接存值的。但像 String str = "abc";这种合下,其字符串值却是保存了一个指向存在栈中数据的引用。 +为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。 + +``` +String str1="abc"; +String str2="abc"; +System.out.println(s1==s2);//true +``` + +注意,这里并不用 str1.equals(str2);的方式,因为这将比较两个字符串的值是否相等。==号,根据 JDK 的说明,只有在两个引用都指向了同一个对象时才返回真值。而我们在这里要看的是,str1 与 str2 是否都指向了同一个对象。 +我们再接着看以下的代码。 + +``` +String str1= new String("abc"); +String str2="abc"; +System.out.println(str1==str2);//false +``` + +创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。   以上两段代码说明,只要是用 new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。 + +f) 数组 + +当定义一个数组,int x[];或 int []x;时,在栈内存中创建一个数组引用,通过该引用(即数组名)来引用数组。x=new int[3];将在堆内存中分配3个保存 int 型数据的空间,堆内存的首地址放到栈内存中,每个数组元素被初始化为0。 + +g) 静态变量 + +用 static 的修饰的变量和方法,实际上是指定了这些变量和方法在内存中的"固定位置"-static storage,可以理解为所有实例对象共有的内存空间。static 变量有点类似于 C 中的全局变量的概念;静态表示的是内存的共享,就是它的每一个实例都指向同一个内存地址。把 static 拿来,就是告诉 JVM 它是静态的,它的引用(含间接引用)都是指向同一个位置,在那个地方,你把它改了,它就不会变成原样,你把它清理了,它就不会回来了。 那静态变量与方法是在什么时候初始化的呢?对于两种不同的类属性,static 属性与 instance 属性,初始化的时机是不同的。instance 属性在创建实例的时候初始化,static属性在类加载,也就是第一次用到这个类的时候初始化,对于后来的实例的创建,不再次进行初始化。 我们常可看到类似以下的例子来说明这个问题: + +``` +class Student{ +static int numberOfStudents=0; +Student() +{ +numberOfStudents++; +} +} +``` + +每一次创建一个新的Student实例时,成员numberOfStudents都会不断的递增,并且所有的Student实例都访问同一个numberOfStudents变量,实际上int numberOfStudents变量在内存中只存储在一个位置上。 + +5. Java 的内存管理实例 + +Java 程序的多个部分(方法,变量,对象)驻留在内存中以下两个位置:即堆和栈,现在我们只关心3类事物:实例变量,局部变量和对象: +实例变量和对象驻留在堆上 +局部变量驻留在栈上 +让我们查看一个 java 程序,看看他的各部分如何创建并且映射到栈和堆中: + +``` +public class Dog { +Collar c; +String name; +//1. main()方法位于栈上 +public static void main(String[] args) { +//2. 在栈上创建引用变量d,但Dog对象尚未存在 +Dog d; +//3. 创建新的Dog对象,并将其赋予d引用变量 +d = new Dog(); +//4. 将引用变量的一个副本传递给go()方法 +d.go(d); +} +//5. 将go()方法置于栈上,并将dog参数作为局部变量 +void go(Dog dog){ +//6. 在堆上创建新的Collar对象,并将其赋予Dog的实例变量 +c =new Collar(); +} +//7.将setName()添加到栈上,并将dogName参数作为其局部变量 +void setName(String dogName){ +//8. name的实例对象也引用String对象 +name=dogName; +} +//9. 程序执行完成后,setName()将会完成并从栈中清除,此时,局部变量dogName也会消失,尽管它所引用的String仍在堆上 +} +``` + +6. 垃圾回收机制: + +(问题一:什么叫垃圾回收机制?) 垃圾回收是一种动态存储管理技术,它自动地释放不再被程序引用的对象,按照特定的垃圾收集算法来实现资源自动回收的功能。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用,以免造成内存泄露。 (问题二:java 的垃圾回收有什么特点?) JAVA 语言不允许程序员直接控制内存空间的使用。内存空间的分配和回收都是由 JRE 负责在后台自动进行的,尤其是无用内存空间的回收操作(garbagecollection,也称垃圾回收),只能由运行环境提供的一个超级线程进行监测和控制。 (问题三:垃圾回收器什么时候会运行?) 一般是在 CPU 空闲或空间不足时自动进行垃圾回收,而程序员无法精确控制垃圾回收的时机和顺序等。 (问题四:什么样的对象符合垃圾回收条件?) 当没有任何获得线程能访问一个对象时,该对象就符合垃圾回收条件。 (问题五:垃圾回收器是怎样工作的?) 垃圾回收器如发现一个对象不能被任何活线程访问时,他将认为该对象符合删除条件,就将其加入回收队列,但不是立即销毁对象,何时销毁并释放内存是无法预知的。垃圾回收不能强制执行,然而 Java 提供了一些方法(如:System.gc()方法),允许你请求 JVM 执行垃圾回收,而不是要求,虚拟机会尽其所能满足请求,但是不能保证 JVM 从内存中删除所有不用的对象。 (问题六:一个 java 程序能够耗尽内存吗?) 可以。垃圾收集系统尝试在对象不被使用时把他们从内存中删除。然而,如果保持太多活的对象,系统则可能会耗尽内存。垃圾回收器不能保证有足够的内存,只能保证可用内存尽可能的得到高效的管理。 (问题七:如何显示的使对象符合垃圾回收条件?) (1) 空引用 :当对象没有对他可到达引用时,他就符合垃圾回收的条件。也就是说如果没有对他的引用,删除对象的引用就可以达到目的,因此我们可以把引用变量设置为 null,来符合垃圾回收的条件。 + +``` +StringBuffer sb = new StringBuffer("hello"); +System.out.println(sb); +sb=null; +``` + +(2) 重新为引用变量赋值:可以通过设置引用变量引用另一个对象来解除该引用变量与一个对象间的引用关系。 + +``` +StringBuffer sb1 = new StringBuffer("hello"); +StringBuffer sb2 = new StringBuffer("goodbye"); +System.out.println(sb1); +sb1=sb2;//此时"hello"符合回收条件 +``` + +(3) 方法内创建的对象:所创建的局部变量仅在该方法的作用期间内存在。一旦该方法返回,在这个方法内创建的对象就符合垃圾收集条件。有一种明显的例外情况,就是方法的返回对象。 + +``` +public static void main(String[] args) { +Date d = getDate(); +System.out.println("d = " + d); +} +private static Date getDate() { +Date d2 = new Date(); +StringBuffer now = new StringBuffer(d2.toString()); +System.out.println(now); +return d2; +} +``` + +(4) 隔离引用:这种情况中,被回收的对象仍具有引用,这种情况称作隔离岛。若存在这两个实例,他们互相引用,并且这两个对象的所有其他引用都删除,其他任何线程无法访问这两个对象中的任意一个。也可以符合垃圾回收条件。 + +``` +public class Island { +Island i; +public static void main(String[] args) { +Island i2 = new Island(); +Island i3 = new Island(); +Island i4 = new Island(); +i2.i=i3; +i3.i=i4; +i4.i=i2; +i2=null; +i3=null; +i4=null; +} +} +``` + +(问题八:垃圾收集前进行清理------finalize()方法) java 提供了一种机制,使你能够在对象刚要被垃圾回收之前运行一些代码。这段代码位于名为 finalize()的方法内,所有类从 Object 类继承这个方法。由于不能保证垃圾回收器会删除某个对象。因此放在 finalize()中的代码无法保证运行。因此建议不要重写 finalize(); +7. final 问题: +final 使得被修饰的变量"不变",但是由于对象型变量的本质是"引用",使得"不变"也有了两种含义:引用本身的不变?,和引用指向的对象不变。? 引用本身的不变: + +``` +final StringBuffer a=new StringBuffer("immutable"); +final StringBuffer b=new StringBuffer("not immutable"); +a=b;//编译期错误 +final StringBuffer a=new StringBuffer("immutable"); +final StringBuffer b=new StringBuffer("not immutable"); +a=b;//编译期错误 +引用指向的对象不变: +final StringBuffer a=new StringBuffer("immutable"); +a.append(" broken!"); //编译通过 +final StringBuffer a=new StringBuffer("immutable"); +a.append(" broken!"); //编译通过 +``` + +可见,final 只对引用的"值"(也即它所指向的那个对象的内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final 是不负责的。这很类似==操作符:==操作符只负责引用的"值"相等,至于这个地址所指向的对象内容是否相等,==操作符是不管的。在举一个例子: + +``` +public class Name { +private String firstname; +private String lastname; +public String getFirstname() { +return firstname; +} +public void setFirstname(String firstname) { +this.firstname = firstname; +} +public String getLastname() { +return lastname; +} +public void setLastname(String lastname) { +this.lastname = lastname; +} +} + +public class Name { +private String firstname; +private String lastname; +public String getFirstname() { +return firstname; +} +public void setFirstname(String firstname) { +this.firstname = firstname; +} +public String getLastname() { +return lastname; +} +public void setLastname(String lastname) { +this.lastname = lastname; +} +} + + 编写测试方法: +public static void main(String[] args) { +final Name name = new Name(); +name.setFirstname("JIM"); +name.setLastname("Green"); +System.out.println(name.getFirstname()+" "+name.getLastname()); +} +public static void main(String[] args) { +final Name name = new Name(); +name.setFirstname("JIM"); +name.setLastname("Green"); +System.out.println(name.getFirstname()+" "+name.getLastname()); +} +``` + +理解 final 问题有很重要的含义。许多程序漏洞都基于此----final 只能保证引用永远指向固定对象,不能保证那个对象的状态不变。在多线程的操作中,一个对象会被多个线程共享或修改,一个线程对对象无意识的修改可能会导致另一个使用此对象的线程崩溃。一个错误的解决方法就是在此对象新建的时候把它声明为 final,意图使得它"永远不变"。其实那是徒劳的。 Final 还有一个值得注意的地方: 先看以下示例程序: + +``` +class Something { +final int i; +public void doSomething() { +System.out.println("i = " + i); +} +} +class Something { +final int i; +public void doSomething() { +System.out.println("i = " + i); +} +} +``` + +对于类变量,Java 虚拟机会自动进行初始化。如果给出了初始值,则初始化为该初始值。如果没有给出,则把它初始化为该类型变量的默认初始值。但是对于用 final 修饰的类变量,虚拟机不会为其赋予初值,必须在 constructor (构造器)结束之前被赋予一个明确的值。可以修改为"final int i = 0;"。 + +8. 如何把程序写得更健壮: +1、尽早释放无用对象的引用。 好的办法是使用临时变量的时候,让引用变量在退出活动域后,自动设置为 null,暗示垃圾收集器来收集该对象,防止发生内存泄露。对于仍然有指针指向的实例,jvm 就不会回收该资源,因为垃圾回收会将值为 null 的对象作为垃圾,提高 GC 回收机制效率; +2、定义字符串应该尽量使用 String str="hello"; 的形式 ,避免使用 String str = new String("hello"); 的形式。因为要使用内容相同的字符串,不必每次都 new 一个 String。例如我们要在构造器中对一个名叫 s 的 String 引用变量进行初始化,把它设置为初始值,应当这样做: + +``` +public class Demo { +private String s; +public Demo() { +s = "Initial Value"; +} +} + +public class Demo { +private String s; +... +public Demo { +s = "Initial Value"; +} +... +} +``` + + 而非 + +``` +s = new String("Initial Value"); +s = new String("Initial Value"); +``` + +后者每次都会调用构造器,生成新对象,性能低下且内存开销大,并且没有意义,因为 String 对象不可改变,所以对于内容相同的字符串,只要一个 String 对象来表示就可以了。也就说,多次调用上面的构造器创建多个对象,他们的 String 类型属性 s 都指向同一个对象。 + +3、我们的程序里不可避免大量使用字符串处理,避免使用 String,应大量使用 StringBuffer ,因为 String 被设计成不可变(immutable)类,所以它的所有对象都是不可变对象,请看下列代码; + +``` +String s = "Hello"; +s = s + " world!"; +String s = "Hello"; +s = s + " world!"; +``` + +在这段代码中,s 原先指向一个 String 对象,内容是 "Hello",然后我们对 s 进行了+操作,那么 s 所指向的那个对象是否发生了改变呢?答案是没有。这时,s 不指向原来那个对象了,而指向了另一个 String 对象,内容为"Hello world!",原来那个对象还存在于内存之中,只是 s 这个引用变量不再指向它了。 通过上面的说明,我们很容易导出另一个结论,如果经常对字符串进行各种各样的修改,或者说,不可预见的修改,那么使用 String 来代表字符串的话会引起很大的内存开销。因为 String 对象建立之后不能再改变,所以对于每一个不同的字符串,都需要一个 String 对象来表示。这时,应该考虑使用 StringBuffer 类,它允许修改,而不是每个不同的字符串都要生成一个新的对象。并且,这两种类的对象转换十分容易。 +4、尽量少用静态变量 ,因为静态变量是全局的,GC 不会回收的; +5、尽量避免在类的构造函数里创建、初始化大量的对象 ,防止在调用其自身类的构造器时造成不必要的内存资源浪费,尤其是大对象,JVM 会突然需要大量内存,这时必然会触发 GC 优化系统内存环境;显示的声明数组空间,而且申请数量还极大。 以下是初始化不同类型的对象需要消耗的时间: + +![](images/36.png) + +从表1可以看出,新建一个对象需要980个单位的时间,是本地赋值时间的980倍,是方法调用时间的166倍,而新建一个数组所花费的时间就更多了。 +6、尽量在合适的场景下使用对象池技术 以提高系统性能,缩减缩减开销,但是要注意对象池的尺寸不宜过大,及时清除无效对象释放内存资源,综合考虑应用运行环境的内存资源限制,避免过高估计运行环境所提供内存资源的数量。 +7、大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理 ,然后解决一块释放一块的策略。 +8、不要在经常调用的方法中创建对象 ,尤其是忌讳在循环中创建对象。可以适当的使用 hashtable,vector 创建一组对象容器,然后从容器中去取那些对象,而不用每次 new 之后又丢弃。 +9、一般都是发生在开启大型文件或跟数据库一次拿了太多的数据,造成 Out Of Memory Error 的状况,这时就大概要计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。 +10、尽量少用 finalize 函数 ,因为 finalize()会加大 GC 的工作量,而 GC 相当于耗费系统的计算能力。 +11、不要过滥使用哈希表 ,有一定开发经验的开发人员经常会使用 hash 表(hash 表在 JDK 中的一个实现就是 HashMap)来缓存一些数据,从而提高系统的运行速度。比如使用 HashMap 缓存一些物料信息、人员信息等基础资料,这在提高系统速度的同时也加大了系统的内存占用,特别是当缓存的资料比较多的时候。其实我们可以使用操作系统中的缓存的概念来解决这个问题,也就是给被缓存的分配一个一定大小的缓存容器,按照一定的算法淘汰不需要继续缓存的对象,这样一方面会因为进行了对象缓存而提高了系统的运行效率,同时由于缓存容器不是无限制扩大,从而也减少了系统的内存占用。现在有很多开源的缓存实现项目,比如 ehcache、oscache 等,这些项目都实现了 FIFO、MRU 等常见的缓存算法 \ No newline at end of file diff --git a/servlet-jsp.md b/servlet-jsp.md new file mode 100644 index 0000000..5e83766 --- /dev/null +++ b/servlet-jsp.md @@ -0,0 +1,610 @@ +# Servlet 与 JSP + +# JSP 与 SERVLET 的关系 + +综述:Java Servlet 是 JSP 技术的基础,而且大型的 Web 应用程序的开发需要 Java Servlet 和 JSP 配合才能完成。现在许多 Web 服务器都支持 Servlet,即使不直接支持 Servlet 的 Web 服务器,也可以通过附件的应用服务器和模块来支持 Servlet,这得益于 Java 的跨平台特性。另外,由于 Servlet 内部以线程方式提供提供服务,不必对于每个请求都启动一个进程,并且利用多线程机制可以同时为多个请求服务,因此 Servlet 的效率非常高。 + +但它并不是没有缺点,和传统的 CGI、ISAPI、NSAPI 方式相同,Java Servlet 也是利用输出 HTML 语句来实现动态网页的,如果用它来开发整个网站,动态部分和静态页面的整合过程将变得无法想象。这就是 SUN 还要推出 JSP 的原因。 + +如何正确理解 servlet? + +servlet 的基本概念 + +一、Servlet 的结构 + +在具体掌握 servlet 之前,须对 Java 语言有所了解。我们假设读者已经具备一定的 Java 基础。在 Servlet API 中最重要的是 Servlet 接口(interface),所有的 servlets 都必须实现该接口,途径有很多:一是直接实现该接口,二是通过扩展类(class)来实现,如 HttpServlet。 这个 Servlet 接口提供了 servlet 与客户端联系的方法。Servlet 编写者可以在他们开发 servlet 程序时提供更多一些或所有的这样方法。 + +当一个 servlet 接收来自客户端的调用请求, 它接收两个对象:一个是 ServletRequest,另外一个是 ServletResponse。这个 ServletRequest 类概括从客户端到服务器之间的联系,而 ServletResponse 类概括从 servlet 返回客户端的联系。 + +ServletRequest 接口可以获取到这样一些信息,如由客户端传送的阐述名称,客户端正在使用的协议,产生请求并且接收请求的服务器远端主机名。它也提供获取数据流的 ServletInputStream, 这些数据是客户端引用中使用 HTTP POST 和 PUT 方法递交的。一个 ServletRequest 的子类可以让 servlet 获取更多的协议特性数据。例如: HttpServletRequest 包含获取 HTTP-specific 头部信息的方法。 + +ServletResponse 接口给出相应客户端的 servlet 方法。它允许 servlet 设置内容长度和回应的 mime 类型,并且提供输出流 ServletOutputStream,通过编写者可以发回相应的数据。 ServletResponse 子类可以给出更多 protocol- specific 内容的信息。 例如:HttpServletResponse 包含允许 servlet 操作 HTTP-specific 头部信息的方法。 + +上面有关类和接口的描述,构成了一个基本的 Servlet 框架。HTTP servlets 有一些附加的可以提供 session-tracking capabilities 的方法。servlet 编写者可以利用这些 API,在有他人操作时维护 servlet 与客户端之间的状态。 + +二、Servlet 的接口 + +我们编写的 Servlet ,一般从 Javax 包的 HttpServlet 类扩展而来,在 HttpServlet 中加入了一些附加的方法,这些方法可以被协助处理 HTTP 基本请求的 HttpServlet 类中的方法 service 自动地调用。这些方法有: + +· doGet 用来处理 HTTP 的 GET 请求。 + +这个 GET 操作仅仅允许客户从 HTTP server 上取得(GET)资源。重载此方法的用户自动允许支持方法 HEAD。这个 GET 操作被认为是安全的,没有任何的负面影响,对用户来说是很可靠的。比如,大多数的正规查询都没有副作用。打算改变存储数据的请求必须用其他的 HTTP 方法。这些方法也必须是个安全的操作。方法 doGet 的缺省实现将返回一个 HTTP 的 BAD_REQUEST 错误。 + +方法 doGet 的格式: + +``` +protected void doGet(HttpServletResquest request, HttpServletResponse response) +throws ServletException,IOException; +``` + +· doPost 用来处理 HTTP 的 POST 请求。 + +这个 POST 操作包含了在必须通过此 servlet 执行的请求中的数据。由于它不能立即取得资源,故对于那些涉及到安全性的用户来说,通过 POST 请求操作会有一些副作用。 + +方法 doPost 的缺省实现将返回一个 HTTP 的 BAD_REQUEST 错误。当编写 servlet 时,为了支持 POST 操作必须在子类 HttpServlet 中实现(implement)此方法。 + +此方法的格式: + +``` +protected void doPost(HttpServletResquest request, HttpServletResponse response) +throws ServletException,IOException; +``` + +· doPut 用来处理 HTTP 的 PUT 请求。 + +此 PUT 操作模拟通过 FTP 发送一个文件。对于那些涉及到安全性的用户来说,通过 PUT 请求操作也会有一些副作用。 + +此方法的格式: + +``` +protected void doPut(HttpServletResquest request,HttpServletResponse response) +throws ServletException,IOException; +``` + +· doDelete 用来处理 HTTP 的 DELETE 请求。 + +此操作允许客户端请求一个从 server 移出的 URL。对于那些涉及到安全性的用户来说,通过 DELETE 请求操作会有一些副作用。 + +方法 doDelete 的缺省实现将返回一个 HTTP 的 BAD_REQUEST 错误。当编写 servlet 时,为了支持 DELETE 操作,必须在子类 HttpServlet 中实现(implement)此方法。 + +此方法的格式: + +``` +protected void doDelete (HttpServletResquest request, HttpServletResponse response) +throws ServletException,IOException; +``` +· doHead 用来处理 HTTP 的 HEAD 请求。 + +缺省地,它会在无条件的 GET 方法执行时运行,但是不返回任何数据到客户端。只返回包含内容信息的长度的 header。由于用到 GET 操作,此方法应该是很安全的(没有副作用)也是可重复使用的。此方法的缺省实现(implement)自动地处理了 HTTPDE 的 HEAD 操作并且不需要通过一个子类实现(implement)。 + +此方法的格式: + +``` +protected void doHead (HttpServletResquest request,HttpServletResponse response) +throws ServletException,IOException; +``` + +· doOptions 用来处理 HTTP 的 OPTIONS 请求。 + +此操作自动地决定支持什么 HTTP 方法。比如说,如果读者创建 HttpServlet 的子类并重载方法 doGet,然后方法 doOptions 会返回下面的 header: + +Allow:GET,HEAD,TRACE,OPTIONS + +一般不需要重载方法 doOptions。 + +此方法的格式: + +``` +protected void doOptions (HttpServletResquest request, HttpServletResponse response) +throws ServletException,IOException; +``` + +· doTrace 用来处理 HTTP 的 TRACE 请求。 + +此方法的缺省实现产生一个包含所有在 trace 请求中的 header 的信息的应答(response)。在开发 servlet 时,多数情况下需要重载此方法。 + +此方法的格式: + +``` +protected void doTrace (HttpServletResquest request, HttpServletResponse response) +throws ServletException,IOException; +``` + +在开发以 HTTP 为基础的 servlet 中,Servlet 开发者关心方法 doGet 和方法 doPost 即可。 + +三、Servlet 的生命周期 + +如果读者写过 Java 的小应用程序(Applet),那 Servlet 对你来说就不会太难,也许更为简单。因为 Servlet 不用考虑图形界面的应用。与小应用程序一样,Servlet 也有一个生命周期。Servlet 的生命周期是当服务器装载运行 servlets:接收来自客户端的多个请求并且返回数据给客户端,然后再删除移开 servlets。下面详细描述如下: + +1.初始化时期 +当一个服务器装载 servlet 时,它运行 servlet 的 init() 方法。 + +``` +public void init(ServletConfig config) throws ServletException +{ +super.init(); //一些初始化的操作,如数据库的连接 +} +``` + +需要记住的是一定要在 init()结束时调用 super.init()。init()方法不能反复调用,一旦调用就是重装载 servlet。直到服务器调用 destroy 方法卸载 servlet 后才能再调用。 + +2.Servlet 的执行时期 +在服务器装载初始化 servlet 后,servlet 就能够处理客户端的请求,我们可以用 service 方法来实现。每个客户端请求有它自己 service 方法:这些方法接收客户端请求,并且发回相应的响应。Servlets 能同时运行多个 service。这是很重要的,这样,service 方法可以按一个thread-safe 样式编写。如:service 方法更新 servlet 对象中的一个字段 field,这个字段是可以同时存取的。假如某个服务器不能同时并发运行 service 方法,也可以用SingleThreadModel 接口。这个接口保证不会有两个以上的线程(threads)并发运行。在 Servlet 执行期间其最多的应用是处理客户端的请求并产生一个网页。其代码如下: + +``` +PrintWriter out = response.getWriter(); +out.println("<html>"); +out.println("<head><title>"# Servlet </title></head>"); +out.println("<body>"); +out.println("Hello World"); +out.println("</body></html>"); +out.close(); +``` + +3.Servlet 结束时期 +Servlets 一直运行到他们被服务器卸载。在结束的时候需要收回在 init()方法中使用的资源,在 Servlet 中是通过 destory()方法来实现的。 + +``` +public void destroy() +{ +//回收在init()中启用的资源,如关闭数据库的连接等。 +} +``` + +JSP 与 servlet 之间是怎样的关系? + +JSP 主要关注于 HTML(或者 XML)与 Java 代码的结合,以及加入其中的 JSP 标记。如果一个支持 JSP 的服务器遇到一个 JSP 页面,它首先查看该页面是否被编译成为一个 servlet。由此可见,JSP 被编译成 servlet,即被转变为纯 Java,然后被装载入服务器执行。当然,这一过程,根据不同的 JSP 引擎而略有不同。 + +JSP 和 servlet 在应用上有什么区别 + +简单的说,SUN 首先发展出 SERVLET,其功能比较强劲,体系设计也很先进,只是,它输出 HTML 语句还是采用了老的 CGI 方式,是一句一句输出,所以,编写和修改 HTML 非常不方便。 + +后来 SUN 推出了类似于 ASP 的嵌套型的 JSP,把 JSP TAG 嵌套到 HTML 语句中,这样,就大大简化和方便了网页的设计和修改。新型的网络语言如 ASP,PHP 都是嵌套型的。 + +从网络三层结构的角度看,一个网络项目最少分三层:data layer,business layer,,presentation layer。当然也可以更复杂。 + +SERVLET 用来写 business layer 是很强大的,但是对于写 presentation layer 就很不方便。JSP 则主要是为了方便写 presentation layer 而设计的。当然也可以写 business layer。写惯了 ASP,PHP,CGI 的朋友,经常会不自觉的把 presentation layer 和 business layer 混在一起。比如把数据库处理信息放到 JSP 中,其实,它应该放在 business layer 中。 + +根据 SUN 自己的推荐,JSP 中应该仅仅存放与 presentation layer 有关的部分,也就是说,只放输出 HTML 网页的部份。而所有的数据计算、数据分析、数据库联结处理,统统是属于 business layer,应该放在 JAVA BEANS 中。通过 JSP 调用 JAVA BEANS,实现两层的整合。 + +实际上,微软前不久推出的 DNA 技术,简单说,就是 ASP+COM/DCOM 技术。与 JSP+BEANS 完全类似,所有的 presentation layer 由 ASP 完成,所有的 business layer 由 COM/DCOM 完成。通过调用,实现整合。 + +为什么要采用这些组件技术呢?因为单纯的 ASP/JSP 语言是非常低效率执行的,如果出现大量用户点击,纯 SCRIPT 语言很快就到达了他的功能上限,而组件技术就能大幅度提高功能上限,加快执行速度。 + +另外一方面,纯 SCRIPT 语言将 presentation layer 和 business layer 混在一起,造成修改不方便,并且代码不能重复利用。如果想修改一个地方,经常会牵涉到十几页 CODE,采用组件技术就只改组件就可以了。 + +综上所述,SERVLET 是一个不完善的产品,写 business layer 很好,写 presentation layer 就很逊色许多了,并且两层混杂。所以,推出 JSP+BAEN,用 JSP 写 presentation layer,用 BAEN 写 business layer。SUN 自己的意思也是将来用 JSP 替代 SERVLET。 + +所以,学了 JSP,不会用 JAVA BEAN 并进行整合,等于没学。 + +如何调用 servlet? + +要调用 Servlet 或 Web 应用程序,请使用下列任一种方法:由 URL 调用、在
标记中调用、在标记中调用、在 ASP 文件中调用。 + +1.由 URL 调用 Servlet + +这里有两种用 Servlet 的 URL 从浏览器中调用该 Servlet 的方法: + +(1)指定 Servlet 名称:当用 WebSphere 应用服务器管理器来将一个 Servlet 实例添加(注册)到服务器配置中时,必须指定"Servlet 名称"参数的值。例如,可以指定将 hi 作为 HelloWorldServlet 的 Servlet 名称。要调用该 Servlet,需打开 http: //your.server.name/servlet/hi。也可以指定 Servlet 和类使用同一名称(HelloWorldServlet)。在这种情况下,将由 http://your.server.name/servlet/ HelloWorldServlet 来调用 Servlet 的实例。 + +(2)指定 Servlet 别名:用 WebSphere 应用服务器 管理器来配置 Servlet 别名,该别名是用于调用 Servlet 的快捷 URL。快捷 URL 中不包括 Servlet 名称。 + +2.在标记中指定 Servlet + +可以在标记中调用 Servlet。HTM 格式使用户能在 Web 页面(即从浏览器)上输入数据,并向 Servlet 提交数据。例如: + +``` + +
    +AM
    +FM
    +
+(用于放置文本输入区域的标记、按钮和其它的提示符。) + +``` + +ACTION 特性表明了用于调用 Servlet 的 URL。关于 METHOD 的特性,如果用户输入的信息是通过 GET 方法向 Servlet 提交的,则 Servlet 必须优先使用 doGet()方法。反之,如果用户输入的信息是通过 POST 方法向 Servlet 提交的,则 Servlet 必须优先使用 doPost()方法。使用 GET 方法时,用户提供的信息是查询字符串表示的 URL 编码。无需对 URL 进行编码,因为这是由表单完成的。然后 URL 编码的查询字符串被附加到 Servlet URL 中,则整个 URL 提交完成。URL 编码的查询字符串将根据用户同可视部件之间的交互操作,将用户所选的值同可视部件的名称进行配对。例如,考虑前面的 HTML 代码段将用于显示按钮(标记为 AM 和 FM),如果用户选择 FM 按钮,则查询字符串将包含 name=value 的配对操作为 broadcast= fm。因为在这种情况下,Servlet 将响应 HTTP 请求,因此 Servlet 应基于 HttpServlet 类。Servlet 应根据提交给它的查询字符串中的用户信息使用的 GET 或 POST 方法,而相应地使用 doGet() 或 doPost() 方法。 + +3.在标记中指定 Servlet + +当使用标记来调用 Servlet 时,如同使用
标记一样,无需创建一个完整的 HTML 页面。作为替代,Servlet 的输出仅是 HTML 页面的一部分,且被动态嵌入到原始 HTML 页面中的其它静态文本中。所有这些都发生在服务器上,且发送给用户的仅是结果 HTML 页面。建议在 Java 服务器页面(JSP)文件中使用 标记。 + +原始 HTML 页面中包含标记。Servlet 将在这两个标记中被调用,且 Servlet 的响应将覆盖这两个标记间的所有东西和标记本身。如果用户的浏览器可以看到HTML源文件,则用户将看不到标记。要在 Domino Go Webserver 上使用该方法,请启用服务器上的服务器端包括功能。部分启用过程将会涉及到添加特殊文件类型 SHTML。当 Web 服务器接收到一个扩展名为 SHTML 的 Web 页面请求时,它将搜索 标记。对于所有支持的 Web 服务器,WebSphere 应用服务器将处理 SERVLET 标记间的所有信息。下列 HTML 代码段显示了如何使用该技术。 + +``` + + + +``` + +使用 NAME 和 CODE 属性带来了使用上的灵活性。可以只使用其中一个属性,也可以同时使用两个属性。NAME 属性指定了 Servlet 的名称(使用 WebSphere 应用服务器管理器配置的),或不带.class 扩展名的 Servlet 类名。CODE 属性指定了 Servlet 类名。使用 WebSphere 应用服务器时,建议指定 NAME 和 CODE,或当 NAME 指定了 Servlet 名称时,仅指定 NAME。如果仅指定了 CODE,则会创建一个 NAME=CODE 的 Servlet 实例。装入的 Servlet 将假设 Servlet 名称与 NAME 属性中指定的名称匹配。然后,其它 SHTML 文件可以成功地使用 NAME 属性来指定 Servlet 的名称,并调用已装入的 Servlet。NAME 的值可以直接在要调用 Servlet 的 URL 中使用。如果 NAME 和 CODE 都存在,且 NAME 指定了一个现有 Servlet,则通常使用 NAME 中指定的 Servlet。由于 Servlet 创建了部分 HTML 文件,所以当创建 Servlet 时,将可能会使用 HttpServlet 的一个子类,并优先使用 doGet()方法(因为 GET 方法是提供信息给 Servlet 的缺省方法)。另一个选项是优先使用 service()方法。另外,CODEBASE 是可选的,它指定了装入 Servlet 的远程系统的 URL。请使用 WebSphere 应用服务器管理器来从 JAR 文件配置远程 Servlet 装入系统。 + +在上述的标记示例中,initparm1 是初始化参数名,value 是该参数的值。可以指定多个"名称-值"对的集合。利用 ServletConfig 对象(被传递到 Servlet 的 init()方法中)的 getInitParameterNames()和 getInitParameter()方法来查找参数名和参数值的字符串数组。在示例中,parm1 是参数名,并在初始化 Servlet 后被才被设置某个值。因为只能通过使用"请求"对象的方法来使用以标记设置的参数,所以服务器必须调用 Servlet service()方法,以从用户处传递请求。要获得有关用户的请求信息,请使用 getParameterNames()、getParameter() 和 getParameterValues()方法。 + +初始化参数是持续的。假设一台客户机通过调用一个包含某些初始化参数的 SHTML 文件来调用 Servlet。并假设第二台客户机通过调用第二个 SHTML 文件来调用同一个 Servlet,且该 SHTML 中未指定任何初始化参数。那么第一次调用 Servlet 时所设置的初始化参数将一直可用,并且通过所有其它 SHTML 文件而调用的所有后继 Servlet 都不会更改该参数。直到 Servlet 调用了 destroy()方法后,才能重新设置初始化参数。例如,如果另一个 SHTML 文件指定了另一个不同的初始化参数值,虽然已此时已装入了 Servlet,但该值仍将被忽略。 + +4.在 ASP 文件中调用 Servlet +如果在 Microsoft Internet Information Server(IIS)上有遗留的 ASP 文件,并且无法将 ASP 文件移植成 JSP 文件时,可用 ASP 文件来调用 Servlet。在 WebSphere 应用服务器中的 ASP 支持包括一个用于嵌入 Servlet 的 ActiveX 控制,下面介绍 ActiveX 控制 AspToServlet 的方法和属性。 + +该方法说明如下: + +(1)String ExecServletToString(String servletName);执行 ServletName,并将其输出返回到一个字符串中。 + +(2)ExecServlet(String servletName);执行 ServletName,并将其输出直接发送至 HTML 页面。 + +(3)String VarValue(String varName);获得一预置变量值(其它格式)。 + +(4)VarValue(String varName, String newVal);设置变量值。变量占据的总大小应小于0.5个千字节(Kbyte)。且仅对配置文件使用这些变量。 + +其属性如下: + += Boolean WriteHeaders;若该属性为真,则 Servlet 提供的标题被写入用户处。缺省值为假。 += Boolean OnTest;若该属性为真,服务器会将消息记录到生成的 HTML 页面中。缺省值为假。 +下列 ASP 脚本示例是以 Microsoft Visual Basic Scripting(VBScript)书写的。 + +``` +<% +' Small sample asp file to show the capabilities of the servlets and the ASP GateWay ... +%> +

Starting the ASP->Java Servlet demo

+<% +' Create a Servlet gateway object and initialize it ... +Set Javaasp = Server.CreateObject("AspToServlet.AspToServlet") +' Setting these properties is only for the sake of demo. +' These are the default values ... +Javaasp.OnTest = False +Javaasp.WriteHeaders = False +' Add several variables ... +Javaasp.VarValue("gal") = "lag" +Javaasp.VarValue("pico")= "ocip" +Javaasp.VarValue("tal") = "lat" +Javaasp.VarValue("paz") = "zap" +Javaasp.VarValue("variable name with spaces") = "variable value with spaces" +%> +
+Lets check the variables +<% +Response.Write("variable gal = ") +Response.Write(Javaasp.VarValue("gal")) +%> +
+<% +Response.Write("variable pico = " & Javaasp.VarValue("pico")) +%> + +
+
+<% +galout = Javaasp.ExecServletToString("SnoopServlet") +If Javaasp.WriteHeaders = True Then +%> +Headers were written <% +Else +%> +Headers were not written <% +End If +Response.Write(galout) +%> +

The End ...

+``` + +如何设置 servlet 类的路径? + +因为各个服务器对访问 servlet 的策略不尽相同,所以在设置 servlet 类路径时应该视情况而定。 +对于开发中的 servlet,只需确认包含 Javax.servlet 的 JAR 文档在您的类路径中,并运用如Javac 的普通开发工具。 +对于 JSDK:JSDK_HOME/servlet.jar +JSDK_HOME/server.jar +对于 Tomcat:TOMCAT_HOME/lib/servlet.jar +对于运行中的 servlet,必须为 servlet 引擎设置类路径,这根据不同的引擎,有不同的配置,如哪些库和目录应包括,哪些不应包括。注:对于 servlet 的动态加载引擎如 JRun, Apache Jserv, Tomcat,包含 servlet 类文件的目录不应在类路径中,而应在 config 文件中配置。否则,servlet 可以运行,但不能被动态再加载。 + +Servlet 2.2 规范认为以下应被容器自动包括,因此您不必把他们手工添加到类路径。 + +· 所有的类应放在 webapp/WEB-INF/classes 目录下 + +· 所有 JAR 文件放在 webapp/WEB-INF/lib 目录下 + +· 对 webapps 的应用体现在文档系统中,对已打包进 JAR 文档的 webapps 的应用应放入容器的 webapps 目录。(例如,TOMCAT_HOME/webapps/myapp.jar) + +另外,由 Gene McKenna(mckenna@meangene.com)撰写的"The Complete CLASSPATH Guide for Servlets"详细叙述了如何为 JavaWebServer 和 Jrun 设置类路径。 + +如何实现 servlet 与 applet 的通信? + +这个例子将向读者展示服务器端程序(Servlet)和小应用程序(Applet)之间是如何完成通信活动的。它由三个文件组成,一个是 sendApplet.Java 文件,用于实现 Applet,一个是 receiveservlet.Java,用于实现 servlet,还有一个是 add -servlet.html,用于调用 Applet。 + +在 sendApplet.Java 文件中,最重要的要属 init()函数和 Send()函数,其中 init()函数用来生成整个 Applet 的用户操作界面,包括消息文本框、发送按钮等等。而消息的发送过程则由 Send()函数来完成。请仔细阅读下面的代码: + +``` +private void Send() +{ +message = sendText.getText(); +//清除用户的输入信息 +sendText.setText(""); +showStatus("Message send!"); +//把输入的字符串转化为 x-www-form-urlencoded 格式 +String queryString = "/servlet/ReceiveServlet?message=" + URLEncoder.encode ( message ) ; +p("Attempting to send:"+message); + +//建立与Servlet的联接,并取得Servelt的输出信息 +try { +connect = (new URL(chatURL,queryString)).openConnection(); +showStatus("open connection!"); +//下次连接不用Cache +connect.setDefaultUseCaches(false); +//这次连接也不用Cache +connect.setUseCaches(false); +//打开淂流用于读数据 +connect.setDoInput(true); +//不能用于写数据 +connect.setDoOutput(false); +//服务器与客户的真正连接 +connect.connect(); +p("Made connection to "+connect); +showStatus("Open Stream!"); +DataInputStream in = new DataInputStream(connect.getInputStream()); +showStatus("reading!"); +message = in.readLine(); +while (message! = null) +{ +//在消息文本框显示Servlet生成的信息 +messageText.setText(message); +message = in.readLine(); +} +}catch(MalformedURLException e2) +{ +System.err.println("MalformedURLException!"); +e2.printStackTrace(System.err); +showStatus("MalformedURLException!"); +}catch(IOException e1) +{ +System.err.println("IOException!"); +e2.printStackTrace(System.err); +showStatus("IOException"); +} +} +``` + +整个 Applet 的详细代码请见 sendApplet.Java。 + +当 Applet 与 Servlet 建立连接后,工作就可以交给 Servlet 了,由它来解析客户端的请求,获得参数 message 的值,然后将适当信息返回给客户端,并由 Applet 进行显示。完成该功能的是 receiveservlet.Java 中的 service()函数: + +``` +public void service (HttpServletRequest req,HttpServletResponse res) +throws ServletException,IOException +{ +res.setContentType("text/plain"); +ServletOutputStream out = res.getOutputStream(); +out.print("receive user message:"); +out.print(req.getParameter("message")); +} +``` + +该 Servlet 的详细源代码请见 receiveservlet.Java。 +最后一个文件是 add-servlet.html,它用来调用 Applet: + +``` + + +sendApplet + + +
+ +
+ + +``` + +如何应用应用 Servlet 进行图象处理? + +我们在处理数据时,有时希望能用图象直观的表述,在这里有一个巧方法,能方便快捷的实现一些简单的图形(不能称之图象),比如条形图,我们不必去用 Java 来生成并显示图象,(Java 生成图象很慢),我们可以这样来作,先用作图工具作一个很小的你需要的图片,再根据你所处理的数据量来实时的加长它,就可以得到所要表述的图例。比如我们在数据库中得到了一组数据,我们从中找出最大的那一个,按比列设定其标签的长度,其它的数据图形则可与它相比,得到的长度,这样,一个简简单单的条形图就出来。但有时一些简单的图形已经不能解决我们实际遇到的情况,比如曲线图就不能用这种方法,这时我们需要生成 Java 图象,也许大家都用过 applet 这样的程序吧,若访问量不大,而实时性又很特殊时(比如股票系统),必须这样用它。但事实上,我们 web 程序大多有前后台之分,前台浏览,后台维护。这样我们可以在后台用 servlet 实时动态定时地生成图象文件,而前台只是查看静态图片,这比你用 applet 来动态产生图象的速度快了不知多少倍,因为 applet 来动态产生图象,有两个地方很费时,一是数据库查询时间,二是 applet 本身生成图象就很慢。下面以一个简单的例子来说明一下怎样生成并写入图象文件,本例注重的是怎样写入图象文件,相信写过 applet 的读者会生成更加漂亮的图象。 + +``` +package test; +import Javax.servlet.*; +import Javax.servlet.http.*; +import Java.io.*; +import Java.util.*; +import Java.awt.image.BufferedImage; +import com.sun.image.codec.jpeg.*; +import Java.awt.image.*; +import Java.awt.*; + +public class Servlet2 extends HttpServlet +{ +public void init(ServletConfig config) throws ServletException { +super.init(config); +} + +public void doGet(HttpServletRequest request, HttpServletResponse response) +throws ServletException, IOException +{ +String sFileName = "e:/temp/name.jpg"; +try{ +FileOutputStream fos = new +FileOutputStream(sFileName); +BufferedImage myImage = new BufferedImage(225, 225,BufferedImage. TYPE_INT_RGB); +Graphics g = myImage.getGraphics(); +g.setColor(Color.white); +g.fillRect(0,0,225,225); +g.setColor(Color.black); +g.drawString("Finance Balance Summary", 40, 15); +g.drawString("Primary", 90, 30); +g.setColor(Color.darkGray); +g.fillRect(15,193,7,7); +g.setColor(Color.black); +g.drawString("% Operating", 25, 200); +g.setColor(Color.yellow); +g.fillRect(130,193,7,7); +g.setColor(Color.black); +g.drawString("% Term", 140, 200); +g.setColor(Color.lightGray); +g.fillRect(15,213,7,7); +g.setColor(Color.black); +g.drawString("% Mortgage", 25, 220); +g.setColor(Color.green); +g.fillRect(130,213,7,7); +g.setColor(Color.black); +g.drawString("% Lease", 140, 220); +JPEGImageEncoder jpg = JPEGCodec.createJPEGEncoder(fos); +jpg.encode(myImage); +}catch (Exception e) +{ +String exceptionThrown = e.toString(); +String sourceOfException = " Method"; +System.out.println("Origional Exception Thrown: " +exceptionThrown + '/r' + '/n'); +System.out.println("Origional SourceOfException: " + sourceOfException +'/r' + '/n'); +} // CatchStatementEnd +} +} +``` + +如何通过 Servlet 调用 JavaBean 输出结果集 + +以此我们通过一个例子进行说明,该例演示了如何通过 Servlet 调用 JavaBean 输出结果集,并打印的方法,共由两个文件组成,一个是 JavaBean,用于实现对数据库的访问,并获得结果集;另一个是 Servlet,主要负责 JavaBean 的调用,并将结果集发送到客户端。 + +在 JavaBean 中,我们将访问 DB2 样例数据库(sample)中的 STAFF 表,至于如何实现对数据库的访问,读者可以参考《JSP 与 JDBC》一章。此外,读者可以通过修改部分参数,来实现对其他数据库、表的访问,达到举一反三的效果。 + +该 JavaBean 的核心是 execute()函数: + +``` +public void execute() +{ +try { +//装载JDBC驱动程序 +Class.forName("COM.ibm.db2.jdbc.app.DB2Driver").newInstance(); +//建立对数据库的连接 +conn = DriverManager.getConnection("jdbc:db2:sample", "db2admin", "db2admin"); +stmt = conn.createStatement(); +String sql = "SELECT * FROM STAFF WHERE DEPT=20"; +//执行查询语句,返回结果集 +ResultSet rs = stmt.executeQuery(sql); +setResult(rs); +} catch (SQLException e) { +} catch (IllegalAccessException e2) { +} catch (ClassNotFoundException e3) { +} catch (InstantiationException e4) {} +} +``` + +JavaBean 的具体源代码请见 Tbean.Java。 + +知道数据是如何获取之后,下面我们来看一下Servlet 是如何来调用上述 JavaBean 的。 + +同样看 service()方法即可(详细源代码请见 Tservlet.Java): + +``` +public void service(HttpServletRequest req, HttpServletResponse res) +throws ServletException, IOException +{ +try { +//实例化JavaBean +Demo.TBean Javabean = new Demo.TBean(); +Javabean.execute(); +ResultSet rs1 = Javabean.getResult(); +PrintWriter out = res.getWriter(); +res.setContentType("text/html"); +out.println(""=; +out.println("

Hello World

"=; +out.println(""=; +while (rs1.next()) +{ +out.println(""=; +for (int i = 1; i <= 7; i++= +out.println(""=; +out.println(""=; +} +out.println("
IDNAMEDEPTJOBYEARSSALARYCOMM
" + rs1.getString(i) + "
"=; +Javabean.Sqlclose(); +} catch (SQLException e) {} +} +``` + +//运行:在 VisualAge for Java 的 IBM Websphere Test Environment 的环境下: +//http://localhost:8080/servlet/Demo.TServlet + +如何用 Servlet 来中断涉及的多线程 + +现在我们已经知道,当服务器要卸载一个 Servlet 时,它会在所有的 service 都已经完成后再调用 destroy()方法。如果程序的操作运行需要很长时间,destroy()被调用时就可能还有其他线程在运行。Servlet 程序员必须保证所有的线程都已经完成。 + +长时间运行响应客户端请求的那些 Servlet 应当保留当前有多少方法在运行的记录。它的 long-running 方法应当周期性地轮流询问以确保它们能够继续运行下去。如果 Servlet 被 destroy()方法调用,那么这个 long-running 方法必须停止工作或清除。 + +举例,变量 serviceCounter 用来统计有多少 service 方法在运行,变量 shuttingDown 显示这个 Servlet 是否被 destroy。每个变量有它自己的获取方法: + +``` +public ShutdownExample extends HttpServlet +{ +private int serviceCounter = 0; +private Boolean shuttingDown; +… +//serviceCounter +protected synchronized void enteringServiceMethod() +{ +serviceCounter++; +} +protected synchronized void leavingServiceMethod() +{ +serviceCounter--; +} +protected synchronized int numServices() +{ +return serviceCounter; +} +//shuttingDown +protected setShuttingDown(Boolean flag) +{ +shuttingDown = flag; +} +protected Boolean isShuttingDown() +{ +return shuttingDown; +} +``` + +这个 service 方法每次在它进入时要增加,而在它返回退出时要减少: + +``` +protected void service(HttpServletRequest req , HttpServletResponse resp) +throws ServletException IOException +{ +enteringServiceMethod(); +try{ +super.service(req , resp); +} +finally {leavingServiceMethod();} +} +``` + +destroy 方法应当检查 serviceCounter,如果存在长时间方式运行的话,设置变量 shuttingDown。这个变量将会让那个正在处理请求的线程知道该结束了。destroy 方法应当等待这几个 service 方法完成,这样就是一个清楚的关闭过程了。 + +``` +public void destroy() +{ +//检查是否有线程在运行,如果有,告诉它们停止 +if (numServices() > 0) +{ +setShuttingDown(true); +} +//等待它们停止 +while(numService() > 0) +{ +try{ +thisThread.sleep(interval); +}catch(InterruptedException e) {} +} +} +``` + +long-running 方法如必要应当检查这个变量,并且解释它们的工作: + +``` +public void doPost(…) +{ +… +for(i = 0; ((i < lotsOfStuffToDo) && !isShuttingDown()); i++) +{ +try{ +partOfLongRunningOperation(i); +}catch (InterruptedException e) {} +} +} +``` diff --git a/socket.md b/socket.md new file mode 100644 index 0000000..58a1a91 --- /dev/null +++ b/socket.md @@ -0,0 +1,301 @@ +# Socket + +# java socket 编程 + +## 一,网络编程中两个主要的问题 + +一个是如何准确的定位网络上一台或多台主机,另一个就是找到主机后如何可靠高效的进行数据传输。 + +在 TCP/IP 协议中 IP 层主要负责网络主机的定位,数据传输的路由,由 IP 地址可以唯一地确定 Internet 上的一台主机。 + +而 TCP 层则提供面向应用的可靠(tcp)的或非可靠(UDP)的数据传输机制,这是网络编程的主要对象,一般不需要关心 IP 层是如何处理数据的。 + +目前较为流行的网络编程模型是客户机/服务器(C/S)结构。即通信双方一方作为服务器等待客户提出请求并予以响应。客户则在需要服务时向服务器提 出申请。服务器一般作为守护进程始终运行,监听网络端口,一旦有客户请求,就会启动一个服务进程来响应该客户,同时自己继续监听服务端口,使后来的客户也 能及时得到服务。 + +## 二,两类传输协议:TCP;UDP + +TCP 是 Tranfer Control Protocol 的简称,是一种面向连接的保证可靠传输的协议。通过 TCP 协议传输,得到的是一个顺序的无差错的数据流。发送方和接收方的成对的两个 socket 之间必须建立连接,以便在 TCP 协议的基础上进行通信,当一个 socket(通常都是 server socket)等待建立连接时,另一个 socket 可以要求进行连接,一旦这两个 socket 连接起来,它们就可以进行双向数据传输,双方都可以进行发送或接收操作。 + +UDP 是 User Datagram Protocol 的简称,是一种无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。 + +比较: + +UDP: +1,每个数据报中都给出了完整的地址信息,因此无需要建立发送方和接收方的连接。 + +2,UDP 传输数据时是有大小限制的,每个被传输的数据报必须限定在 64 KB 之内。 + +3,UDP 是一个不可靠的协议,发送方所发送的数据报并不一定以相同的次序到达接收方 + +TCP: +1,面向连接的协议,在 socket 之间进行数据传输之前必然要建立连接,所以在 TCP 中需要连接时间。 + +2,TCP 传输数据大小限制,一旦连接建立起来,双方的 socket 就可以按统一的格式传输大的数据。 + +3,TCP 是一个可靠的协议,它确保接收方完全正确地获取发送方所发送的全部数据。 + +应用: + +1,TCP 在网络通信上有极强的生命力,例如远程连接(Telnet)和文件传输(FTP)都需要不定长度的数据被可靠地传输。但是可靠的传输是要付出代价的,对数据内容正确性的检验必然占用计算机的处理时间和网络的带宽,因此 TCP 传输的效率不如 UDP 高。 + +2,UDP 操作简单,而且仅需要较少的监护,因此通常用于局域网高可靠性的分散系统中 client/server 应用程序。例如视频会议系统,并不要求音频视频数据绝对的正确,只要保证连贯性就可以了,这种情况下显然使用 UDP 会更合理一些。 + +## 三,基于 Socket 的 java 网络编程 + +1,什么是 Socket + +网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个 Socket。Socket 通常用来实现客户方和服务方的连接。Socket 是 TCP/IP 协议的一个十分流行的编程界面,一个 Socket 由一个 IP 地址和一个端口号唯一确定。 + +但是,Socket 所支持的协议种类也不光 TCP/IP 一种,因此两者之间是没有必然联系的。在 Java 环境下,Socket 编程主要是指基于 TCP/IP 协议的网络编程。 + +2,Socket 通讯的过程 + +Server 端 Listen(监听)某个端口是否有连接请求,Client 端向 Server 端发出 Connect(连接)请求,Server 端向 Client 端发回 Accept(接受)消息。一个连接就建立起来了。Server 端和 Client 端都可以通过 Send,Write 等方法与对方通信。 + +对于一个功能齐全的 Socket,都要包含以下基本结构,其工作过程包含以下四个基本的步骤: + +(1) 创建 Socket; + +(2) 打开连接到 Socket 的输入/出流; + +(3) 按照一定的协议对 Socket 进行读/写操作; + +(4) 关闭 Socket.(在实际应用中,并未使用到显示的 close,虽然很多文章都推荐如此,不过在我的程序中,可能因为程序本身比较简单,要求不高,所以并未造成什么影响。) + +3,创建 Socket + +创建 Socket + +java 在包 java.net 中提供了两个类 Socket 和 ServerSocket,分别用来表示双向连接的客户端和服务端。这是两个封装得非常好的类,使用很方便。其构造方法如下: + +``` +Socket(InetAddress address, int port); + +Socket(InetAddress address, int port, boolean stream); + +Socket(String host, int prot); + +Socket(String host, int prot, boolean stream); + +Socket(SocketImpl impl) + +Socket(String host, int port, InetAddress localAddr, int localPort) + +Socket(InetAddress address, int port, InetAddress localAddr, int localPort) + +ServerSocket(int port); + +ServerSocket(int port, int backlog); + +ServerSocket(int port, int backlog, InetAddress bindAddr) +``` + +其中 address、host 和 port 分别是双向连接中另一方的 IP 地址、主机名和端口号,stream 指明 socket 是流 socket 还是数据报 socket,localPort 表示本地主机的端口号,localAddr 和 bindAddr 是本地机器的地址(ServerSocket 的主机地址),impl 是 socket 的父类,既可以用来创建 serverSocket 又可以用来创建 Socket。count 则表示服务端所能支持的最大连接数。例如:学习视频网 http://www.xxspw.com + +``` +Socket client = new Socket("127.0.01.", 80); + +ServerSocket server = new ServerSocket(80); +``` + +注意,在选择端口时,必须小心。每一个端口提供一种特定的服务,只有给出正确的端口,才能获得相应的服务。0~1023的端口号为系统所保留,例如 http 服务的端口号为80,telnet 服务的端口号为21,ftp 服务的端口号为23, 所以我们在选择端口号时,最好选择一个大于1023的数以防止发生冲突。 + +在创建 socket 时如果发生错误,将产生 IOException,在程序中必须对之作出处理。所以在创建 Socket 或 ServerSocket 是必须捕获或抛出例外。 + +4,简单的 Client/Server 程序 + +1. 客户端程序 + +``` +  import java.io.*; + +  import java.net.*; + +  public class TalkClient { + +    public static void main(String args[]) { + +      try{ + +        Socket socket=new Socket("127.0.0.1",4700); + +        //向本机的4700端口发出客户请求 + +        BufferedReader sin=new BufferedReader(new InputStreamReader(System.in)); + +        //由系统标准输入设备构造BufferedReader对象 + +        PrintWriter os=new PrintWriter(socket.getOutputStream()); + +        //由Socket对象得到输出流,并构造PrintWriter对象 + +        BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream())); + +        //由Socket对象得到输入流,并构造相应的BufferedReader对象 + +        String readline; + +        readline=sin.readLine(); //从系统标准输入读入一字符串 + +        while(!readline.equals("bye")){ + +        //若从标准输入读入的字符串为 "bye"则停止循环 + +          os.println(readline); + +          //将从系统标准输入读入的字符串输出到Server + +          os.flush(); + +          //刷新输出流,使Server马上收到该字符串 + +          System.out.println("Client:"+readline); + +          //在系统标准输出上打印读入的字符串 + +          System.out.println("Server:"+is.readLine()); + +          //从Server读入一字符串,并打印到标准输出上 + +          readline=sin.readLine(); //从系统标准输入读入一字符串 + +        } //继续循环 + +        os.close(); //关闭Socket输出流 + +        is.close(); //关闭Socket输入流 + +        socket.close(); //关闭Socket + +      }catch(Exception e) { + +        System.out.println("Error"+e); //出错,则打印出错信息 + +      } + +  } + +} +``` + + 2. 服务器端程序 + +``` +  import java.io.*; + +  import java.net.*; + +  import java.applet.Applet; + +  public class TalkServer{ + +    public static void main(String args[]) { + +      try{ + +        ServerSocket server=null; + +        try{ + +          server=new ServerSocket(4700); + +        //创建一个ServerSocket在端口4700监听客户请求 + +        }catch(Exception e) { + +          System.out.println("can not listen to:"+e); + +        //出错,打印出错信息 + +        } + +        Socket socket=null; + +        try{ + +          socket=server.accept(); + +          //使用accept()阻塞等待客户请求,有客户 + +          //请求到来则产生一个Socket对象,并继续执行 + +        }catch(Exception e) { + +          System.out.println("Error."+e); + +          //出错,打印出错信息 + +        } + +        String line; + +        BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream())); + +         //由Socket对象得到输入流,并构造相应的BufferedReader对象 + +        PrintWriter os=newPrintWriter(socket.getOutputStream()); + +         //由Socket对象得到输出流,并构造PrintWriter对象 + +        BufferedReader sin=new BufferedReader(new InputStreamReader(System.in)); + +         //由系统标准输入设备构造BufferedReader对象 + +        System.out.println("Client:"+is.readLine()); + +        //在标准输出上打印从客户端读入的字符串 + +        line=sin.readLine(); + +        //从标准输入读入一字符串 + +        while(!line.equals("bye")){ + +        //如果该字符串为 "bye",则停止循环 + +          os.println(line); + +          //向客户端输出该字符串 + +          os.flush(); + +          //刷新输出流,使Client马上收到该字符串 + +          System.out.println("Server:"+line); + +          //在系统标准输出上打印读入的字符串 + +          System.out.println("Client:"+is.readLine()); + +          //从Client读入一字符串,并打印到标准输出上 + +          line=sin.readLine(); + +          //从系统标准输入读入一字符串 + +        }  //继续循环 + +        os.close(); //关闭Socket输出流 + +        is.close(); //关闭Socket输入流 + +        socket.close(); //关闭Socket + +        server.close(); //关闭ServerSocket + +      }catch(Exception e){ + +        System.out.println("Error:"+e); + +        //出错,打印出错信息 + +      } + +    } + +  } +``` + +5,支持多客户的 client/server 程序 + +前面的 Client/Server 程序只能实现 Server 和一个客户的对话。在实际应用中,往往是在服务器上运行一个永久的程序,它可以接收来自其他多个客户端的请求,提供相应的服务。为了实现在服务器方给多个客户提供服务的功能,需要对上 面的程序进行改造,利用多线程实现多客户机制。服务器总是在指定的端口上监听是否有客户请求,一旦监听到客户请求,服务器就会启动一个专门的服务线程来响 应该客户的请求,而服务器本身在启动完线程之后马上又进入监听状态,等待下一个客户的到来。 \ No newline at end of file diff --git a/sort.md b/sort.md new file mode 100644 index 0000000..1a692f2 --- /dev/null +++ b/sort.md @@ -0,0 +1,686 @@ +# 排序算法 + +# 各种排序算法的分析及 java 实现 + +排序一直以来都是让我很头疼的事,以前上《数据结构》打酱油去了,整个学期下来才勉强能写出个冒泡排序。由于下半年要准备工作了,也知道排序算法的重要性(据说是面试必问的知识点),所以又花了点时间重新研究了一下。 + +排序大的分类可以分为两种:内排序和外排序。在排序过程中,全部记录存放在内存,则称为内排序,如果排序过程中需要使用外存,则称为外排序。下面讲的排序都是属于内排序。 + +内排序有可以分为以下几类: +(1)、插入排序:直接插入排序、二分法插入排序、希尔排序。 +(2)、选择排序:简单选择排序、堆排序。 +(3)、交换排序:冒泡排序、快速排序。 +(4)、归并排序 +(5)、基数排序 + +**一、插入排序** + +- 思想:每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的字序列的合适位置,直到全部插入排序完为止。 +- 关键问题:在前面已经排好序的序列中找到合适的插入位置。 +- 方法: +–直接插入排序 +–二分插入排序 +–希尔排序 + +①直接插入排序(从后向前找到合适位置后插入) + +1、基本思想:每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的字序列的合适位置(从后向前找到合适位置后),直到全部插入排序完为止。 + +2、实例 + +![](images/5.png) + +3、java 实现 + +``` +package com.sort; + +public class 直接插入排序 { + + public static void main(String[] args) { + int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1}; + System.out.println("排序之前:"); + for (int i = 0; i < a.length; i++) { + System.out.print(a[i]+" "); + } + //直接插入排序 + for (int i = 1; i < a.length; i++) { + //待插入元素 + int temp = a[i]; + int j; + /*for (j = i-1; j>=0 && a[j]>temp; j--) { + //将大于temp的往后移动一位 + a[j+1] = a[j]; + }*/ + for (j = i-1; j>=0; j--) { + //将大于temp的往后移动一位 + if(a[j]>temp){ + a[j+1] = a[j]; + }else{ + break; + } + } + a[j+1] = temp; + } + System.out.println(); + System.out.println("排序之后:"); + for (int i = 0; i < a.length; i++) { + System.out.print(a[i]+" "); + } + } + +} +``` + +4、分析 + +直接插入排序是稳定的排序。关于各种算法的稳定性分析可以参考 http://www.cnblogs.com/Braveliu/archive/2013/01/15/2861201.html + +文件初态不同时,直接插入排序所耗费的时间有很大差异。若文件初态为正序,则每个待插入的记录只需要比较一次就能够找到合适的位置插入,故算法的时间复杂度为 O(n),这时最好的情况。若初态为反序,则第i个待插入记录需要比较 i+1 次才能找到合适位置插入,故时间复杂度为 O(n2),这时最坏的情况。 + +直接插入排序的平均时间复杂度为 O(n2)。 + +②二分法插入排序(按二分法找到合适位置插入) + +1、基本思想:二分法插入排序的思想和直接插入一样,只是找合适的插入位置的方式不同,这里是按二分法找到合适的位置,可以减少比较的次数。 + +2、实例 + +![](images/2.jpg) + +3、java 实现 + +``` +package com.sort; + +public class 二分插入排序 { + public static void main(String[] args) { + int[] a={49,38,65,97,176,213,227,49,78,34,12,164,11,18,1}; + System.out.println("排序之前:"); + for (int i = 0; i < a.length; i++) { + System.out.print(a[i]+" "); + } + //二分插入排序 + sort(a); + System.out.println(); + System.out.println("排序之后:"); + for (int i = 0; i < a.length; i++) { + System.out.print(a[i]+" "); + } + } + + private static void sort(int[] a) { + for (int i = 0; i < a.length; i++) { + int temp = a[i]; + int left = 0; + int right = i-1; + int mid = 0; + while(left<=right){ + mid = (left+right)/2; + if(temp= left; j--) { + a[j+1] = a[j]; + } + if(left != i){ + a[left] = temp; + } + } + } +} +``` + +4、分析 + +当然,二分法插入排序也是稳定的。 + +二分插入排序的比较次数与待排序记录的初始状态无关,仅依赖于记录的个数。当 n 较大时,比直接插入排序的最大比较次数少得多。但大于直接插入排序的最小比较次数。算法的移动次数与直接插入排序算法的相同,最坏的情况为 n2/2,最好的情况为 n,平均移动次数为 O(n2)。 + +③希尔排序 + +1、基本思想:先取一个小于 n 的整数 d1 作为第一个增量,把文件的全部记录分成 d1 个组。所有距离为 d1 的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量 d2=0&&a[j]>temp;j=j-d){ + a[j+d] = a[j]; + } + a[j+d] = temp; + } + } + if(d == 1){ + break; + } + } + System.out.println(); + System.out.println("排序之后:"); + for (int i = 0; i < a.length; i++) { + System.out.print(a[i]+" "); + } + } + +} +``` + +4、分析 + +我们知道一次插入排序是稳定的,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。 + +希尔排序的时间性能优于直接插入排序,原因如下: + +(1)当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。 +(2)当 n 值较小时,n 和 n2 的差别也较小,即直接插入排序的最好时间复杂度 O(n) 和最坏时间复杂度 0(n2) 差别不大。 +(3)在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量 di 逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按 di-1 作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。 +   +因此,希尔排序在效率上较直接插人排序有较大的改进。 +  +希尔排序的平均时间复杂度为 O(nlogn)。 + + +**二、选择排序** +- 思想:每趟从待排序的记录序列中选择关键字最小的记录放置到已排序表的最前位置,直到全部排完。 +- 关键问题:在剩余的待排序记录序列中找到最小关键码记录。 +- 方法: +–直接选择排序 +–堆排序 + +①简单的选择排序 +1、基本思想:在要排序的一组数中,选出最小的一个数与第一个位置的数交换;然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。 +   +2、实例 + +![](images/7.png) + +3、java 实现 + +``` +package com.sort; + +//不稳定 +public class 简单的选择排序 { + + public static void main(String[] args) { + int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1,8}; + System.out.println("排序之前:"); + for (int i = 0; i < a.length; i++) { + System.out.print(a[i]+" "); + } + //简单的选择排序 + for (int i = 0; i < a.length; i++) { + int min = a[i]; + int n=i; //最小数的索引 + for(int j=i+1;j=h2i,hi>=2i+1)或(hi<=h2i,hi<=2i+1) (i=1,2,...,n/2)时称之为堆。在这里只讨论满足前者条件的堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项(大顶堆)。完全二 叉树可以很直观地表示堆的结构。堆顶为根,其它为左子树、右子树。 + +思想:初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储序,使之成为一个 堆,这时堆的根节点的数最大。然后将根节点与堆的最后一个节点交换。然后对前面(n-1)个数重新调整使之成为堆。依此类推,直到只有两个节点的堆,并对 它们作交换,最后得到有 n 个节点的有序序列。从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。 + +2、实例 + +初始序列:46,79,56,38,40,84 + +建堆: + +![](images/8.png) + +交换,从堆中踢出最大数 + +![](images/9.png) + +依次类推:最后堆中剩余的最后两个结点交换,踢出一个,排序完成。 + +3、java 实现 + +``` +package com.sort; +//不稳定 +import java.util.Arrays; + +public class HeapSort { + public static void main(String[] args) { + int[] a={49,38,65,97,76,13,27,49,78,34,12,64}; + int arrayLength=a.length; + //循环建堆 + for(int i=0;i=0;i--){ + //k保存正在判断的节点 + int k=i; + //如果当前k节点的子节点存在 + while(k*2+1<=lastIndex){ + //k节点的左子节点的索引 + int biggerIndex=2*k+1; + //如果biggerIndex小于lastIndex,即biggerIndex+1代表的k节点的右子节点存在 + if(biggerIndexa[j+1]){ + int temp = a[j]; + a[j] = a[j+1]; + a[j+1] = temp; + } + } + } + System.out.println(); + System.out.println("排序之后:"); + for (int i = 0; i < a.length; i++) { + System.out.print(a[i]+" "); + } + } +} +``` + +4、分析 + +冒泡排序是一种稳定的排序方法。  + +- 若文件初状为正序,则一趟起泡就可完成排序,排序码的比较次数为 n-1,且没有记录移动,时间复杂度是 O(n) +- 若文件初态为逆序,则需要n-1趟起泡,每趟进行n-i次排序码的比较,且每次比较都移动三次,比较和移动次数均达到最大值∶O(n2) +- 起泡排序平均时间复杂度为 O(n2) + +②快速排序 +1、基本思想:选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟扫描,将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。 +   +2、实例 + +![](images/11.png) + +3、java 实现 + +``` +package com.sort; + +//不稳定 +public class 快速排序 { + public static void main(String[] args) { + int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1,8}; + System.out.println("排序之前:"); + for (int i = 0; i < a.length; i++) { + System.out.print(a[i]+" "); + } + //快速排序 + quick(a); + System.out.println(); + System.out.println("排序之后:"); + for (int i = 0; i < a.length; i++) { + System.out.print(a[i]+" "); + } + } + + private static void quick(int[] a) { + if(a.length>0){ + quickSort(a,0,a.length-1); + } + } + + private static void quickSort(int[] a, int low, int high) { + if(low=temp){ + high--; + } + a[low] = a[high]; + while(low0){ + max = max/10; + times++; + } + //建立十个队列 + List queue = new ArrayList(); + for (int i = 0; i < 10; i++) { + ArrayList queue1 = new ArrayList(); + queue.add(queue1); + } + //进行times次分配和收集 + for (int i = 0; i < times; i++) { + //分配 + for (int j = 0; j < array.length; j++) { + int x = array[j]%(int)Math.pow(10, i+1)/(int)Math.pow(10, i); + ArrayList queue2 = queue.get(x); + queue2.add(array[j]); + queue.set(x,queue2); + } + //收集 + int count = 0; + for (int j = 0; j < 10; j++) { + while(queue.get(j).size()>0){ + ArrayList queue3 = queue.get(j); + array[count] = queue3.get(0); + queue3.remove(0); + count++; + } + } + } + } +} +``` + +4、分析 + +基数排序是稳定的排序算法。 + +基数排序的时间复杂度为 O(d(n+r)),d 为位数,r 为基数。 + +**总结:** + +一、稳定性: + +稳定:冒泡排序、插入排序、归并排序和基数排序 + +不稳定:选择排序、快速排序、希尔排序、堆排序 + +二、平均时间复杂度 + +O(n^2):直接插入排序,简单选择排序,冒泡排序。 + +在数据规模较小时(9W 内),直接插入排序,简单选择排序差不多。当数据较大时,冒泡排序算法的时间代价最高。性能为 O(n^2) 的算法基本上是相邻元素进行比较,基本上都是稳定的。 + +O(nlogn):快速排序,归并排序,希尔排序,堆排序。 + +其中,快排是最好的, 其次是归并和希尔,堆排序在数据量很大时效果明显。 + +三、排序算法的选择 + +1.数据规模较小 + +(1)待排序列基本序的情况下,可以选择直接插入排序; + +(2)对稳定性不作要求宜用简单选择排序,对稳定性有要求宜用插入或冒泡 + +2.数据规模不是很大 + +(1)完全可以用内存空间,序列杂乱无序,对稳定性没有要求,快速排序,此时要付出 log(N)的额外空间。 + +(2)序列本身可能有序,对稳定性有要求,空间允许下,宜用归并排序 + +3.数据规模很大 + +(1)对稳定性有求,则可考虑归并排序。 + +(2)对稳定性没要求,宜用堆排序 + +4.序列初始基本有序(正序),宜用直接插入,冒泡 + +参考资料: + +http://blog.csdn.net/without0815/article/details/7697916 +http://gengning938.blog.163.com/blog/static/128225381201141121326346/ \ No newline at end of file diff --git a/spring.md b/spring.md new file mode 100644 index 0000000..83dce06 --- /dev/null +++ b/spring.md @@ -0,0 +1,132 @@ +# Spring + +# Spring 系列: Spring 框架简介 +Spring AOP 和 IOC 容器入门 +在这由三部分组成的介绍 Spring 框架的系列文章的第一期中,将开始学习如何用 Spring 技术构建轻量级的、强壮的 J2EE 应用程序。developerWorks 的定期投稿人 Naveen Balani 通过介绍 Spring 框架开始了他由三部分组成的 Spring 系列,其中还将介绍 Spring 面向方面的编程(AOP)和控制反转(IOC)容器。 + +Spring 是一个开源框架,是为了解决企业应用程序开发复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许您选择使用哪一个组件,同时为 J2EE 应用程序开发提供集成的框架。 +在这篇由三部分组成的 Spring 系列 的第 1 部分中,我将介绍 Spring 框架。我先从框架底层模型的角度描述该框架的功能,然后将讨论两个最有趣的模块:Spring 面向方面编程(AOP)和控制反转 (IOC) 容器。接着将使用几个示例演示 IOC 容器在典型应用程序用例场景中的应用情况。这些示例还将成为本系列后面部分进行的展开式讨论的基础,在本文的后面部分,将介绍 Spring 框架通过 Spring AOP 实现 AOP 构造的方式。 +请参阅 [下载](http://www.ibm.com/developerworks/cn/java/wa-spring1/#download),下载 Spring 框架和 Apache Ant,运行本系列的示例应用程序需要它们。 +## Spring 框架 +Spring 框架是一个分层架构,由 7 个定义良好的模块组成。Spring 模块构建在核心容器之上,核心容器定义了创建、配置和管理 bean 的方式,如图 1 所示。 +**图 1. Spring 框架的 7 个模块** + +![](images/9.gif) + +组成 Spring 框架的每个模块(或组件)都可以单独存在,或者与其他一个或多个模块联合实现。每个模块的功能如下: +- **核心容器**:核心容器提供 Spring 框架的基本功能。核心容器的主要组件是 BeanFactory,它是工厂模式的实现。BeanFactory 使用控制反转 (IOC) 模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。 +- **Spring 上下文**:Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息。Spring 上下文包括企业服务,例如 JNDI、EJB、电子邮件、国际化、校验和调度功能。 +- **Spring AOP**:通过配置管理特性,Spring AOP 模块直接将面向方面的编程功能集成到了 Spring 框架中。所以,可以很容易地使 Spring 框架管理的任何对象支持 AOP。Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖 EJB 组件,就可以将声明性事务管理集成到应用程序中。 +- **Spring DAO**:JDBC DAO 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)。Spring DAO 的面向 JDBC 的异常遵从通用的 DAO 异常层次结构。 +- **Spring ORM**:Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括 JDO、Hibernate 和 iBatis SQL Map。所有这些都遵从 Spring 的通用事务和 DAO 异常层次结构。 +- **Spring Web 模块**:Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文。所以,Spring 框架支持与 Jakarta Struts 的集成。Web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。 +- **Spring MVC 框架**:MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的,MVC 容纳了大量视图技术,其中包括 JSP、Velocity、Tiles、iText 和 POI。 +Spring 框架的功能可以用在任何 J2EE 服务器中,大多数功能也适用于不受管理的环境。Spring 的核心要点是:支持不绑定到特定 J2EE 服务的可重用业务和数据访问对象。毫无疑问,这样的对象可以在不同 J2EE 环境 (Web 或 EJB)、独立应用程序、测试环境之间重用。 + +## IOC 和 AOP +控制反转模式(也称作依赖性介入)的基本概念是:不创建对象,但是描述创建它们的方式。在代码中不直接与对象和服务连接,但在配置文件中描述哪一个组件需要哪一项服务。容器 (在 Spring 框架中是 IOC 容器) 负责将这些联系在一起。 +在典型的 IOC 场景中,容器创建了所有对象,并设置必要的属性将它们连接在一起,决定什么时间调用方法。下表列出了 IOC 的一个实现模式。 +- 类型 1 服务需要实现专门的接口,通过接口,由对象提供这些服务,可以从对象查询依赖性(例如,需要的附加服务) +- 类型 2 通过 JavaBean 的属性(例如 setter 方法)分配依赖性 +- 类型 3 依赖性以构造函数的形式提供,不以 JavaBean 属性的形式公开 +Spring 框架的 IOC 容器采用类型 2 和类型3 实现。 +### 面向方面的编程 +面向方面的编程,即 AOP,是一种编程技术,它允许程序员对横切关注点或横切典型的职责分界线的行为(例如日志和事务管理)进行模块化。AOP 的核心构造是方面,它将那些影响多个类的行为封装到可重用的模块中。 +AOP 和 IOC 是补充性的技术,它们都运用模块化方式解决企业应用程序开发中的复杂问题。在典型的面向对象开发方式中,可能要将日志记录语句放在所有方法和 Java 类中才能实现日志功能。在 AOP 方式中,可以反过来将日志服务模块化,并以声明的方式将它们应用到需要日志的组件上。当然,优势就是 Java 类不需要知道日志服务的存在,也不需要考虑相关的代码。所以,用 Spring AOP 编写的应用程序代码是松散耦合的。 +AOP 的功能完全集成到了 Spring 事务管理、日志和其他各种特性的上下文中。 + +## IOC 容器 +Spring 设计的核心是 org.springframework.beans 包,它的设计目标是与 JavaBean 组件一起使用。这个包通常不是由用户直接使用,而是由服务器将其用作其他多数功能的底层中介。下一个最高级抽象是 BeanFactory 接口,它是工厂设计模式的实现,允许通过名称创建和检索对象。BeanFactory 也可以管理对象之间的关系。 +BeanFactory 支持两个对象模型。 +- 单态 模型提供了具有特定名称的对象的共享实例,可以在查询时对其进行检索。Singleton 是默认的也是最常用的对象模型。对于无状态服务对象很理想。 +- 原型 模型确保每次检索都会创建单独的对象。在每个用户都需要自己的对象时,原型模型最适合。 +bean 工厂的概念是 Spring 作为 IOC 容器的基础。IOC 将处理事情的责任从应用程序代码转移到框架。正如我将在下一个示例中演示的那样,Spring 框架使用 JavaBean 属性和配置数据来指出必须设置的依赖关系。 +## BeanFactory 接口 +因为 org.springframework.beans.factory.BeanFactory 是一个简单接口,所以可以针对各种底层存储方法实现。最常用的 BeanFactory 定义是 XmlBeanFactory,它根据 XML 文件中的定义装入 bean,如清单 1 所示。 +**清单 1. XmlBeanFactory** + +``` +BeanFactory factory = new XMLBeanFactory(new FileInputSteam("mybean.xml")); +``` + +在 XML 文件中定义的 Bean 是被消极加载的,这意味在需要 bean 之前,bean 本身不会被初始化。要从 BeanFactory 检索 bean,只需调用 getBean() 方法,传入将要检索的 bean 的名称即可,如清单 2 所示。 +**清单 2. getBean()** + +``` +MyBean mybean = (MyBean) factory.getBean("mybean"); +``` + +每个 bean 的定义都可以是 POJO (用类名和 JavaBean 初始化属性定义) 或 FactoryBean。FactoryBean 接口为使用 Spring 框架构建的应用程序添加了一个间接的级别。 + +## IOC 示例 +理解控制反转最简单的方式就是看它的实际应用。在对由三部分组成的 Spring 系列 的第 1 部分进行总结时,我使用了一个示例,演示了如何通过 Spring IOC 容器注入应用程序的依赖关系(而不是将它们构建进来)。 +我用开启在线信用帐户的用例作为起点。对于该实现,开启信用帐户要求用户与以下服务进行交互: +- 信用级别评定服务,查询用户的信用历史信息。 +- 远程信息链接服务,插入客户信息,将客户信息与信用卡和银行信息连接起来,以进行自动借记(如果需要的话)。 +- 电子邮件服务,向用户发送有关信用卡状态的电子邮件。 + +## 三个接口 +对于这个示例,我假设服务已经存在,理想的情况是用松散耦合的方式把它们集成在一起。以下清单显示了三个服务的应用程序接口。 +**清单 3. CreditRatingInterface** + +``` +public interface CreditRatingInterface { + public boolean getUserCreditHistoryInformation(ICustomer iCustomer); +} +``` + +清单 3 所示的信用级别评定接口提供了信用历史信息。它需要一个包含客户信息的 Customer 对象。该接口的实现是由 CreditRating 类提供的。 +**清单 4. CreditLinkingInterface** + +``` +public interface CreditLinkingInterface { +public String getUrl(); + public void setUrl(String url); + public void linkCreditBankAccount() throws Exception ; +} +``` + +信用链接接口将信用历史信息与银行信息(如果需要的话)连接在一起,并插入用户的信用卡信息。信用链接接口是一个远程服务,它的查询是通过 getUrl() 方法进行的。URL 由 Spring 框架的 bean 配置机制设置,我稍后会讨论它。该接口的实现是由 CreditLinking 类提供的。 +**清单 5. EmailInterface** + +``` +public interface EmailInterface { + public void sendEmail(ICustomer iCustomer); + public String getFromEmail(); + public void setFromEmail(String fromEmail) ; + public String getPassword(); + public void setPassword(String password) ; + public String getSmtpHost() ; + public void setSmtpHost(String smtpHost); + public String getUserId() ; + public void setUserId(String userId); + } +``` + +EmailInterface 负责向客户发送关于客户信用卡状态的电子邮件。邮件配置参数(例如 SMPT 主机、用户名、口令)由前面提到的 bean 配置机制设置。Email 类提供了该接口的实现。 + +## Spring 使其保持松散 +这些接口就位之后,接下来要考虑的就是如何用松散耦合方式将它们集成在一起。在 清单 6 中可以看到信用卡帐户用例的实现。 +注意,所有的 setter 方法都是由 Spring 的配置 bean 实现的。所有的依赖关系 (也就是三个接口)都可以由 Spring 框架用这些 bean 注入。createCreditCardAccount() 方法会用服务去执行其余实现。在 清单 7 中可以看到 Spring 的配置文件。我用箭头突出了这些定义。 + +## 运行应用程序 +要运行示例应用程序,首先必须 下载 Spring 框架 及其所有依赖文件。接下来,将框架释放到(比如说)磁盘 c:\,这会创建 C:\spring-framework-1.2-rc2 (适用于当前发行版本) 这样的文件夹。在继续后面的操作之前,还必须下载和释放 Apache Ant。 +接下来,将源代码释放到文件夹,例如 c:\ 盘,然后创建 SpringProject。将 Spring 库(即 C:\spring-framework-1.2-rc2\dist 下的 spring.jar 和 C:\spring-framework-1.2-rc2\lib\jakarta-commons 下的 commons-logging.jar)复制到 SpringProject\lib 文件夹中。完成这些工作之后,就有了必需的构建依赖关系集。 +打开命令提示符,将当前目录切换到 SpringProject,在命令提示符中输入以下命令:build。 +这会构建并运行 CreateCreditAccountClient 类,类的运行将创建 Customer 类对象并填充它,还会调用 CreateCreditCardAccount 类创建并链接信用卡帐户。CreateCreditAccountClient 还会通过 ClassPathXmlApplicationContext 装入 Spring 配置文件。装入 bean 之后,就可以通过 getBean() 方法访问它们了,如清单 8 所示。 +**清单 8. 装入 Spring 配置文件** + +``` +ClassPathXmlApplicationContext appContext = + new ClassPathXmlApplicationContext(new String[] { + "springexample-creditaccount.xml" + }); +CreateCreditCardAccountInterface creditCardAccount = + (CreateCreditCardAccountInterface) + appContext.getBean("createCreditCard"); +``` + +## 结束语 +在这篇由三部分组成的 Spring 系列 的第一篇文章中,我介绍了 Spring 框架的基础。我从讨论组成 Spring 分层架构的 7 个模块开始,然后深入介绍了其中两个模块:Spring AOP 和 IOC 容器。 +由于学习的最佳方法是实践,所以我用一个工作示例介绍了 IOC 模式 (像 Spring 的 IOC 容器实现的那样)如何用松散耦合的方式将分散的系统集成在一起。在这个示例中可以看到,将依赖关系或服务注入工作中的信用卡帐户应用程序,要比从头开始构建它们容易得多。 +请继续关注这一系列的下一篇文章,我将在这里学习的知识基础上,介绍 Spring AOP 模块如何在企业应用程序中提供持久支持,并让您开始了解 Spring MVC 模块和相关插件。 \ No newline at end of file diff --git a/string-array.md b/string-array.md new file mode 100644 index 0000000..a338250 --- /dev/null +++ b/string-array.md @@ -0,0 +1,281 @@ +# 字符串与数组 + +# Java 语言基础知识之字符串数组 + +java 语言中,数组是一种最简单的复合数据类型。数组是有序数据的集合,数组中的每个元素具有相同的数据类型,可以用一个统一的数组名和下标来唯一地确定数组中的元素。数组有一维数组和多维数组。 + +### 2.4.1 一维数组 + +1. 一维数组的定义 + +``` +  type arrayName[ ]; +  类型(type)可以为Java中任意的数据类型,包括简单类型和复合类型。 +  例如: +   int intArray[ ]; +   Date dateArray[]; +``` + +2.一维数组的初始化 + +``` +  ◇ 静态初始化 +    int intArray[]={1,2,3,4}; +    String stringArray[]={"abc", "How", "you"}; + +  ◇ 动态初始化 +    1)简单类型的数组 +    int intArray[]; +    intArray = new int[5]; + +   2)复合类型的数组 +    String stringArray[ ]; +    String stringArray = new String[3];/*为数组中每个元素开辟引用 +                      空间(32位) */ +    stringArray[0]= new String("How");//为第一个数组元素开辟空间 +    stringArray[1]= new String("are");//为第二个数组元素开辟空间 +    stringArray[2]= new String("you");// 为第三个数组元素开辟空间 +``` + +3.一维数组元素的引用 + +数组元素的引用方式为: + +``` +     arrayName[index] +``` + +index 为数组下标,它可以为整型常数或表达式,下标从0开始。每个数组都有一个属性 length 指明它的长度,例如:intArray.length 指明数组 intArray 的长度。 + +### 2.4.2 多维数组 + +Java 语言中,多维数组被看作数组的数组。 + +1.二维数组的定义 + +``` +  type arrayName[ ][ ]; +  type [ ][ ]arrayName; +``` + +2.二维数组的初始化 + +``` +  ◇ 静态初始化 +  int intArray[ ][ ]={{1,2},{2,3},{3,4,5}}; + +  Java语言中,由于把二维数组看作是数组的数组,数组空间不是连续分配的,所以不要求二维数组每一维的大小相同。 + +  ◇ 动态初始化 +  1) 直接为每一维分配空间,格式如下: +  arrayName = new type[arrayLength1][arrayLength2]; +  int a[ ][ ] = new int[2][3]; + +  2) 从最高维开始,分别为每一维分配空间: +  arrayName = new type[arrayLength1][ ]; +  arrayName[0] = new type[arrayLength20]; +  arrayName[1] = new type[arrayLength21]; +  … +  arrayName[arrayLength1-1] = new type[arrayLength2n]; + +  3) 例: +  二维简单数据类型数组的动态初始化如下, +  int a[ ][ ] = new int[2][ ]; +  a[0] = new int[3]; +  a[1] = new int[5]; +``` + +对二维复合数据类型的数组,必须首先为最高维分配引用空间,然后再顺次为低维分配空间。 +而且,必须为每个数组元素单独分配空间。 + +例如: + +``` +  String s[ ][ ] = new String[2][ ]; +  s[0]= new String[2];//为最高维分配引用空间 +  s[1]= new String[2]; //为最高维分配引用空间 +  s[0][0]= new String("Good");// 为每个数组元素单独分配空间 +  s[0][1]= new String("Luck");// 为每个数组元素单独分配空间 +  s[1][0]= new String("to");// 为每个数组元素单独分配空间 +  s[1][1]= new String("You");// 为每个数组元素单独分配空间 +``` + +3.二维数组元素的引用 +   +对二维数组中的每个元素,引用方式为:arrayName[index1][index2] +例如: num[1][0]; + +4.二维数组举例: + +【例2.2】两个矩阵相乘 + +``` +  public class MatrixMultiply{ +   public static void main(String args[]){ +   int i,j,k; +   int a[][]=new int [2][3]; //动态初始化一个二维数组 +   int b[][]={{1,5,2,8},{5,9,10,-3},{2,7,-5,-18}};//静态初始化 +                           一个二维数组 +   int c[][]=new int[2][4]; //动态初始化一个二维数组 +   for (i=0;i<2;i++) +     for (j=0; j<3 ;j++) +      a[i][j]=(i+1)*(j+2); +   for (i=0;i<2;i++){ +     for (j=0;j<4;j++){ +      c[i][j]=0; +   for(k=0;k<3;k++) +     c[i][j]+=a[i][k]*b[k][j]; +      } +     } +   System.out.println("*******Matrix C********");//打印Matrix C标记 +   for(i=0;i<2;i++){ +     for (j=0;j<4;j++) +      System.out.println(c[i][j]+" "); +     System.out.println(); +      } +     } +   } +``` + +## 2.5 字符串的处理 + +### 2.5.1 字符串的表示 + +Java 语言中,把字符串作为对象来处理,类 String 和 StringBuffer 都可以用来表示一个字符串。(类名都是大写字母打头) + +``` + 1.字符串常量 + +  字符串常量是用双引号括住的一串字符。 +    "Hello World!" + + 2.String 表示字符串常量 + +  用String表示字符串: +  String( char chars[ ] ); +  String( char chars[ ], int startIndex, int numChars ); +  String( byte ascii[ ], int hiByte ); +  String( byte ascii[ ], int hiByte, int startIndex, int numChars ); +  String 使用示例: +  String s=new String() ; 生成一个空串 + +  下面用不同方法生成字符串"abc": +  char chars1[]={'a','b','c'}; +  char chars2[]={'a','b','c','d','e'}; +  String s1=new String(chars1); +  String s2=new String(chars2,0,3); +  byte ascii1[]={97,98,99}; +  byte ascii2[]={97,98,99,100,101}; +  String s3=new String(ascii1,0); +  String s4=new String(ascii2,0,0,3); + + 3.用 StringBuffer 表示字符串 + +  StringBuffer( ); /*分配16个字符的缓冲区*/ +  StringBuffer( int len ); /*分配 len 个字符的缓冲区*/ +  StringBuffer( String s ); /*除了按照s的大小分配空间外,再分配16个 +               字符的缓冲区*/ +``` + +### 2.5.2 访问字符串 + +1.类 String 中提供了 length( )、charAt( )、indexOf( )、lastIndexOf( )、getChars( )、getBytes( )、toCharArray( )等方法。 + +``` +  ◇ public int length() 此方法返回字符串的字符个数 +  ◇ public char charAt(int index) 此方法返回字符串中 index 位置上的字符,其中index 值的 范围是 0~length-1 +  ◇ public int indexOf(int ch) +    public lastIndexOf(in ch) +   +  返回字符 ch 在字符串中出现的第一个和最后一个的位置 +  ◇ public int indexOf(String str) +    public int lastIndexOf(String str) +  返回子串 str 中第一个字符在字符串中出现的第一个和最后一个的位置 +  ◇ public int indexOf(int ch,int fromIndex) +    public lastIndexOf(in ch ,int fromIndex) +  返回字符 ch 在字符串中位置 fromIndex 以后出现的第一个和最后一个的位置 +  ◇ public int indexOf(String str,int fromIndex) +    public int lastIndexOf(String str,int fromIndex) +  返回子串 str 中的第一个字符在字符串中位置 fromIndex 后出现的第一个和最后一个的位置。 +  ◇ public void getchars(int srcbegin,int end ,char buf[],int dstbegin) +   srcbegin 为要提取的第一个字符在源串中的位置, end 为要提取的最后一个字符在源串中的位置,字符数组 buf[]存放目的字符串,    dstbegin 为提取的字符串在目的串中的起始位置。 +  ◇public void getBytes(int srcBegin, int srcEnd,byte[] dst, int dstBegin) +  参数及用法同上,只是串中的字符均用8位表示。 +``` + +2.类 StringBuffer 提供了 length( )、charAt( )、getChars( )、capacity()等方法。 + +方法 capacity()用来得到字符串缓冲区的容量,它与方法 length()所返回的值通常是不同的。 + +### 2.5.3 修改字符串 + +修改字符串的目的是为了得到新的字符串,类 String 和类 StringBuffer 都提供了相应的方法。有关各个方法的使用,参考 java 2 API。 + +``` + 1.String 类提供的方法: + +   concat( ) +   replace( ) +   substring( ) +   toLowerCase( ) +   toUpperCase( ) + +  ◇ public String contat(String str); +  用来将当前字符串对象与给定字符串str连接起来。 +  ◇ public String replace(char oldChar,char newChar); +  用来把串中出现的所有特定字符替换成指定字符以生成新串。 +  ◇ public String substring(int beginIndex); +  public String substring(int beginIndex,int endIndex); +  用来得到字符串中指定范围内的子串。 +  ◇ public String toLowerCase(); +  把串中所有的字符变成小写。 +  ◇ public String toUpperCase(); +  把串中所有的字符变成大写。 +``` + +``` + 2.StringBuffer 类提供的方法: + +  append( ) +  insert( ) +  setCharAt( ) + +  如果操作后的字符超出已分配的缓冲区,则系统会自动为它分配额外的空间。 +  ◇ public synchronized StringBuffer append(String str); +  用来在已有字符串末尾添加一个字符串 str。 +  ◇ public synchronized StringBuffer insert(int offset, String str); +  用来在字符串的索引offset位置处插入字符串 str。 +  ◇ public synchronized void setCharAt(int index,char ch); +  用来设置指定索引 index 位置的字符值。 + +  注意:String 中对字符串的操作不是对源操作串对象本身进行的,而是对新生成的一个源操作串对象的拷贝进行的,其操作的结果不影响源串。 + +  相反,StringBuffer 中对字符串的连接操作是对源串本身进行的,操作之后源串的值发生了变化,变成连接后的串。 +``` + +### 2.5.4 其它操作 + +1.字符串的比较 + +String 中提供的方法: +equals( )和 equalsIgnoreCase( ) +它们与运算符'= ='实现的比较是不同的。运算符'= ='比较两个对象是否引用同一个实例,而equals( )和 equalsIgnoreCase( )则比较两个字符串中对应的每个字符值是否相同。 + +2.字符串的转化 + +java.lang.Object 中提供了方法 toString( )把对象转化为字符串。 + +3.字符串"+"操作 + +运算符'+'可用来实现字符串的连接: +String s = "He is "+age+" years old."; +其他类型的数据与字符串进行"+"运算时,将自动转换成字符串。具体过程如下: +String s=new StringBuffer("he is").append(age).append("years old").toString(); + +注意:除了对运算符"+"进行了重载外,java 不支持其它运算符的重载。 + +【本讲小结】 + +java 中的数据类型有简单数据类型和复合数据类型两种,其中简单数据类型包括整数类型、浮点类型、字符类型和布尔类型;复合数据类型包含类、接口和数组。表达式是由运算符和操作数组成的符号序列,对一个表达式进行运算时,要按运算符的优先顺序从高向低进行,同级的运算符则按从左到右的方向进行。条件语句、循环语句和跳转语句是 java 中常用的控制语句。 + +数组是最简单的复合数据类型,数组是有序数据的集合,数组中的每个元素具有相同的数据类型,可以用一个统一的数组名和下标来唯一地确定数组中的元素。Java 中,对数组定义时并不为数组元素分配内存,只有初始化后,才为数组中的每一个元素分配空间。已定义的数组必须经过初始化后,才可以引用。数组的初始化分为静态初始化和动态初始化两种,其中对复合数据类型数组动态初始化时,必须经过两步空间分配:首先,为数组开辟每个元素的引用空间;然后,再为每个数组元素开辟空间。Java 中把字符串当作对象来处理, java.lang.String 类提供了一系列操作字符串的方法,使得字符串的生成、访问和修改等操作容易和规范。 \ No newline at end of file diff --git a/struts.md b/struts.md new file mode 100644 index 0000000..ece3408 --- /dev/null +++ b/struts.md @@ -0,0 +1,229 @@ +# Struts2 + +# Struts2 的核心和工作原理 + +在学习 struts2 之前,首先我们要明白使用 struts2 的目的是什么?它能给我们带来什么样的好处? + +**设计目标** + +Struts 设计的第一目标就是使 MVC 模式应用于 web 程序设计。在这儿 MVC 模式的好处就不在提了。 + +**技术优势** +Struts2 有两方面的技术优势,一是所有的 Struts2 应用程序都是基于 client/server HTTP交换协议,The Java Servlet API 揭示了 Java Servlet 只是 Java API 的一个很小子集,这样我们可以在业务逻辑部分使用功能强大的 Java 语言进行程序设计。 + +二是提供了对 MVC 的一个清晰的实现,这一实现包含了很多参与对所以请求进行处理的关键组件,如:拦截器、OGNL 表达式语言、堆栈。 + +因为 struts2 有这样目标,并且有这样的优势,所以,这是我们学习 struts2 的理由,下面,我们在深入剖析一下 struts 的工作原理。 + +**工作原理** +Suruts2 的工作原理可以用下面这张图来描述,下面我们分步骤介绍一下每一步的核心内容 + +![](images/1.jpg) + +一个请求在 Struts2 框架中的处理大概分为以下几个步骤 + +1、客户端初始化一个指向 Servlet 容器(例如 Tomcat)的请求 + +2、这个请求经过一系列的过滤器(Filter)(这些过滤器中有一个叫做 ActionContextCleanUp的可选过滤器,这个过滤器对于 Struts2 和其他框架的集成很有帮助,例如:SiteMesh Plugin) + +3、接着 FilterDispatcher 被调用,FilterDispatcher 询问 ActionMapper 来决定这个请是否需要调用某个 Action,FilterDispatcher 是控制器的核心,就是 mvc 中 c 控制层的核心。下面粗略的分析下我理解的 FilterDispatcher 工作流程和原理:FilterDispatcher 进行初始化并启用核心 doFilter + +``` +public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException ...{ + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + ServletContext servletContext = filterConfig.getServletContext(); + // 在这里处理了HttpServletRequest和HttpServletResponse。 + DispatcherUtils du = DispatcherUtils.getInstance(); + du.prepare(request, response);//正如这个方法名字一样进行locale、encoding以及特殊request parameters设置 + try ...{ + request = du.wrapRequest(request, servletContext);//对request进行包装 + } catch (IOException e) ...{ + String message = "Could not wrap servlet request with MultipartRequestWrapper!"; + LOG.error(message, e); + throw new ServletException(message, e); + } + ActionMapperIF mapper = ActionMapperFactory.getMapper();//得到action的mapper + ActionMapping mapping = mapper.getMapping(request);// 得到action 的 mapping + if (mapping == null) ...{ + // there is no action in this request, should we look for a static resource? + String resourcePath = RequestUtils.getServletPath(request); + if ("".equals(resourcePath) && null != request.getPathInfo()) ...{ + resourcePath = request.getPathInfo(); + } + if ("true".equals(Configuration.get(WebWorkConstants.WEBWORK_SERVE_STATIC_CONTENT)) + && resourcePath.startsWith("/webwork")) ...{ + String name = resourcePath.substring("/webwork".length()); + findStaticResource(name, response); + } else ...{ + // this is a normal request, let it pass through + chain.doFilter(request, response); + } + // WW did its job here + return; + } + Object o = null; + try ...{ + //setupContainer(request); + o = beforeActionInvocation(request, servletContext); +//整个框架最最核心的方法,下面分析 + du.serviceAction(request, response, servletContext, mapping); + } finally ...{ + afterActionInvocation(request, servletContext, o); + ActionContext.setContext(null); + } + } +du.serviceAction(request, response, servletContext, mapping); +//这个方法询问ActionMapper是否需要调用某个Action来处理这个(request)请求,如果ActionMapper决定需要调用某个Action,FilterDispatcher把请求的处理交给ActionProxy + +public void serviceAction(HttpServletRequest request, HttpServletResponse response, String namespace, String actionName, Map requestMap, Map parameterMap, Map sessionMap, Map applicationMap) ...{ + HashMap extraContext = createContextMap(requestMap, parameterMap, sessionMap, applicationMap, request, response, getServletConfig()); //实例化Map请求 ,询问ActionMapper是否需要调用某个Action来处理这个(request)请求 + extraContext.put(SERVLET_DISPATCHER, this); + OgnlValueStack stack = (OgnlValueStack) request.getAttribute(ServletActionContext.WEBWORK_VALUESTACK_KEY); + if (stack != null) ...{ + extraContext.put(ActionContext.VALUE_STACK,new OgnlValueStack(stack)); + } + try ...{ + ActionProxy proxy = ActionProxyFactory.getFactory().createActionProxy(namespace, actionName, extraContext); +//这里actionName是通过两道getActionName解析出来的, FilterDispatcher把请求的处理交给ActionProxy,下面是ServletDispatcher的 TODO: + request.setAttribute(ServletActionContext.WEBWORK_VALUESTACK_KEY, proxy.getInvocation().getStack()); + proxy.execute(); + //通过代理模式执行ActionProxy + if (stack != null)...{ + request.setAttribute(ServletActionContext.WEBWORK_VALUESTACK_KEY,stack); + } + } catch (ConfigurationException e) ...{ + log.error("Could not find action", e); + sendError(request, response, HttpServletResponse.SC_NOT_FOUND, e); + } catch (Exception e) ...{ + log.error("Could not execute action", e); + sendError(request, response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e); + } +} +``` + +4、如果 ActionMapper 决定需要调用某个 Action,FilterDispatcher 把请求的处理交给 ActionProxy + +5、ActionProxy 通过 ConfigurationManager 询问框架的配置文件,找到需要调用的 Action 类 ,这里,我们一般是从 struts.xml 配置中读取。 + +6、ActionProxy 创建一个 ActionInvocation 的实例。 + +7、ActionInvocation 实例使用命名模式来调用,在调用 Action 的过程前后,涉及到相关拦截器(Intercepter)的调用。 + +下面我们来看看 ActionInvocation 是如何工作的: + +ActionInvocation 是 Xworks 中 Action 调度的核心。而对 Interceptor 的调度,也正是由 ActionInvocation 负责。ActionInvocation 是一个接口,而 DefaultActionInvocation 则是 Webwork 对 ActionInvocation 的默认实现。 + +Interceptor 的调度流程大致如下: + +1. ActionInvocation 初始化时,根据配置,加载 Action 相关的所有 Interceptor。 + +2. 通过 ActionInvocation.invoke 方法调用 Action 实现时,执行 Interceptor。 + +Interceptor 将很多功能从我们的 Action 中独立出来,大量减少了我们 Action 的代码,独立出来的行为具有很好的重用性。XWork、WebWork 的许多功能都是有 Interceptor 实现,可以在配置文件中组装 Action 用到的 Interceptor,它会按照你指定的顺序,在 Action 执行前后运行。 + +这里,我们简单的介绍一下 Interceptor + +在 struts2 中自带了很多拦截器,在 struts2-core-2.1.6.jar 这个包下的 struts-default.xml 中我们可以发现: + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +对于 sturts2 自带的拦截器,使用起来就相对比较方便了,我们只需要在 struts.xml 的 action 标签中加入 并且 struts.xml 扩展 struts-default,就可以使用 + +如果是要自定义拦截器,首先需要写一个拦截器的类: + +``` +package ceshi; +import com.opensymphony.xwork2.ActionInvocation; +import com.opensymphony.xwork2.interceptor.AbstractInterceptor; + +publicclassAuthorizationInterceptor extends AbstractInterceptor { + + @Override + public Stringintercept(ActionInvocation ai)throws Exception { + + System.out.println("abc"); + return ai.invoke(); + + } + +} +``` + +并且在 struts.xml 中进行配置 + +``` + + + + + + + + + + + /success.jsp + + + +``` + +8、一旦 Action 执行完毕,ActionInvocation 负责根据 struts.xml 中的配置找到对应的返回结果。返回结果通常是(但不总是,也可能是另外的一个 Action 链)一个需要被表示的 JSP 或者 FreeMarker 的模版。在表示的过程中可以使用 Struts2 框架中继承的标签。在这个过程中需要涉及到 ActionMapper + +在上述过程中所有的对象(Action,Results,Interceptors,等)都是通过 ObjectFactory 来创建的。 + +**Struts2 和 struts1 的比较** + +struts2 相对于 struts1 来说简单了很多,并且功能强大了很多,我们可以从几个方面来看: + +**从体系结构来看:**struts2 大量使用拦截器来出来请求,从而允许与业务逻辑控制器 与 servlet-api 分离,避免了侵入性;而 struts1.x 在 action 中明显的侵入了 servlet-api. + +**从线程安全分析:**struts2.x 是线程安全的,每一个对象产生一个实例,避免了线程安全问题;而 struts1.x 在 action 中属于单线程。 + +**性能方面:**struts2.x 测试可以脱离 web 容器,而 struts1.x 依赖 servlet-api,测试需要依赖 web 容器。 + +**请求参数封装对比:**struts2.x 使用 ModelDriven 模式,这样我们直接封装 model 对象,无需要继承任何 struts2 的基类,避免了侵入性。 + +**标签的优势:**标签库几乎可以完全替代 JSTL 的标签库,并且 struts2.x 支持强大的 ognl 表达式。 + +当然,struts2 和 struts1 相比,在文件上传,数据校验等方面也 方便了好多。在这就不详谈了。 + +一个比较优秀的框架可以帮着我们更高效,稳定的开发合格的产品,不过我们也不要依赖框架,我们只要理解了思想,设计模式,我们可以自己扩展功能,不然 就要 永远让别人牵着走了! \ No newline at end of file diff --git a/type-operation.md b/type-operation.md new file mode 100644 index 0000000..913010f --- /dev/null +++ b/type-operation.md @@ -0,0 +1,174 @@ +# 基本类型与运算符 + +# Java 基本数据类型 + +java 基本类型 +作者:臧圩人 + +基本类型,或者叫做内置类型,是 JAVA 中不同于类的特殊类型。它们是我们编程中使用最频繁的类型,因此面试题中也总少不了它们的身影,在这篇文章中我们将从面试中常考的几个方面来回顾一下与基本类型相关的知识。 + +基本类型共有八种,它们分别都有相对应的包装类。关于它们的详细信息请看下表: + +基本类型可以分为三类,字符类型 char,布尔类型 boolean 以及数值类型 byte、short、int、long、float、double。数值类型又可以分为整数类型 byte、short、int、long 和浮点数类型 float、double。JAVA 中的数值类型不存在无符号的,它们的取值范围是固定的,不会随着机器硬件环境或者操作系统的改变而改变。实际上,JAVA 中还存在另外一种基本类型 void,它也有对应的包装类 java.lang.Void,不过我们无法直接对它们进行操作。对于数值类型的基本类型的取值范围,我们无需强制去记忆,因为它们的值都已经以常量的形式定义在对应的包装类中了。请看下面的例子: + +Java代码 + +``` +public class PrimitiveTypeTest { + public static void main(String[] args) { + // byte + System.out.println("基本类型:byte 二进制位数:" + Byte.SIZE); + System.out.println("包装类:java.lang.Byte"); + System.out.println("最小值:Byte.MIN_VALUE=" + Byte.MIN_VALUE); + System.out.println("最大值:Byte.MAX_VALUE=" + Byte.MAX_VALUE); + System.out.println(); + + // short + System.out.println("基本类型:short 二进制位数:" + Short.SIZE); + System.out.println("包装类:java.lang.Short"); + System.out.println("最小值:Short.MIN_VALUE=" + Short.MIN_VALUE); + System.out.println("最大值:Short.MAX_VALUE=" + Short.MAX_VALUE); + System.out.println(); + + // int + System.out.println("基本类型:int 二进制位数:" + Integer.SIZE); + System.out.println("包装类:java.lang.Integer"); + System.out.println("最小值:Integer.MIN_VALUE=" + Integer.MIN_VALUE); + System.out.println("最大值:Integer.MAX_VALUE=" + Integer.MAX_VALUE); + System.out.println(); + + // long + System.out.println("基本类型:long 二进制位数:" + Long.SIZE); + System.out.println("包装类:java.lang.Long"); + System.out.println("最小值:Long.MIN_VALUE=" + Long.MIN_VALUE); + System.out.println("最大值:Long.MAX_VALUE=" + Long.MAX_VALUE); + System.out.println(); + + // float + System.out.println("基本类型:float 二进制位数:" + Float.SIZE); + System.out.println("包装类:java.lang.Float"); + System.out.println("最小值:Float.MIN_VALUE=" + Float.MIN_VALUE); + System.out.println("最大值:Float.MAX_VALUE=" + Float.MAX_VALUE); + System.out.println(); + + // double + System.out.println("基本类型:double 二进制位数:" + Double.SIZE); + System.out.println("包装类:java.lang.Double"); + System.out.println("最小值:Double.MIN_VALUE=" + Double.MIN_VALUE); + System.out.println("最大值:Double.MAX_VALUE=" + Double.MAX_VALUE); + System.out.println(); + + // char + System.out.println("基本类型:char 二进制位数:" + Character.SIZE); + System.out.println("包装类:java.lang.Character"); + // 以数值形式而不是字符形式将Character.MIN_VALUE输出到控制台 + System.out.println("最小值:Character.MIN_VALUE=" + + (int) Character.MIN_VALUE); + // 以数值形式而不是字符形式将Character.MAX_VALUE输出到控制台 + System.out.println("最大值:Character.MAX_VALUE=" + + (int) Character.MAX_VALUE); + } +} +``` + +运行结果: + +1、基本类型:byte 二进制位数:8 +2、包装类:java.lang.Byte +3、最小值:Byte.MIN_VALUE=-128 +4、最大值:Byte.MAX_VALUE=127 +5、 +6、基本类型:short 二进制位数:16 +7、包装类:java.lang.Short +8、最小值:Short.MIN_VALUE=-32768 +9、最大值:Short.MAX_VALUE=32767 +10、 +11、基本类型:int 二进制位数:32 +12、包装类:java.lang.Integer +13、最小值:Integer.MIN_VALUE=-2147483648 +14、最大值:Integer.MAX_VALUE=2147483647 +15、 +16、基本类型:long 二进制位数:64 +17、包装类:java.lang.Long +18、最小值:Long.MIN_VALUE=-9223372036854775808 +19、最大值:Long.MAX_VALUE=9223372036854775807 +20、 +21、基本类型:float 二进制位数:32 +22、包装类:java.lang.Float +23、最小值:Float.MIN_VALUE=1.4E-45 +24、最大值:Float.MAX_VALUE=3.4028235E38 +25、 +26、基本类型:double 二进制位数:64 +27、包装类:java.lang.Double +28、最小值:Double.MIN_VALUE=4.9E-324 +29、最大值:Double.MAX_VALUE=1.7976931348623157E308 +30、 +31、基本类型:char 二进制位数:16 +32、包装类:java.lang.Character +33、最小值:Character.MIN_VALUE=0 +34、最大值:Character.MAX_VALUE=65535 + +Float 和 Double 的最小值和最大值都是以科学记数法的形式输出的,结尾的“E+数字”表示 E 之前的数字要乘以10的多少倍。比如3.14E3就是3.14×1000=3140,3.14E-3就是3.14/1000=0.00314。 + +大家将运行结果与上表信息仔细比较就会发现 float、double 两种类型的最小值与Float.MIN_VALUE、 Double.MIN_VALUE 的值并不相同,这是为什么呢?实际上Float.MIN_VALUE 和 Double.MIN_VALUE 分别指的是 float 和 double 类型所能表示的最小正数。也就是说存在这样一种情况,0到 ±Float.MIN_VALUE 之间的值 float 类型无法表示,0 到 ±Double.MIN_VALUE 之间的值 double 类型无法表示。这并没有什么好奇怪的,因为这些范围内的数值超出了它们的精度范围。 + +基本类型存储在栈中,因此它们的存取速度要快于存储在堆中的对应包装类的实例对象。从 Java5.0(1.5)开始,JAVA 虚拟机(Java Virtual Machine)可以完成基本类型和它们对应包装类之间的自动转换。因此我们在赋值、参数传递以及数学运算的时候像使用基本类型一样使用它们的包装类,但这并不意味着你可以通过基本类型调用它们的包装类才具有的方法。另外,所有基本类型(包括 void)的包装类都使用了 final 修饰,因此我们无法继承它们扩展新的类,也无法重写它们的任何方法。 + +# JAVA 基础之 java 运算符大百科 + +运算符用于执行程序代码运算,会针对一个以上操作数项目来进行运算。下面介绍 JAVA 中的运算符。 + +**一、算术运算符:** +单目:+(取正)-(取负) ++(自增1) - -(自减1) 双目:+ - * / %(取余) 三目:a>b?true:false 说明:当 a 大于 b 的时候,为 true(也就是冒号之前的值),否则为false;这整个运算符包括一个关系运算符(可以是">""<""!="等等),一个"?",一个":",冒号前后需要有两个表达式或者是值或者是对象。 + +**二、关系运算:** +等于符号:==,不等于符号: != ,大于符号:>, 小于符号:<,大于等于符号: >= ,小于等于符号: <= . + +**三、位运算符 逻辑运算符:** +位运算符 与(&)、非(~)、或(|)、异或(^)&:当两边操作数的位同时为1时,结果为1,否则为0.如1100&1010=1000 | :当两边操作数的位有一边为1时,结果为0,否则为1.如1100|1010=1110 ~:0变1,1变0 ^:两边的位不同时,结果为1,否则为0.如1100^1010=0110 逻辑运算符 与(&&)、非(!)、或(||) + +**四、赋值运算符** += += -= *= /= %= &= ^= |= 《= 》= + +**五、instanceof 运算符** +该运算符是双目运算符,左面的操作元是一个对象,右面是一个类。当左面的对象是右面的类创建的对象时,该运算符运算结果是 true,否则是 false. + +**六、 运算符综述** +Java 的表达式就是用运算符连接起来的符合 Java 规则的式子。运算符的优先级决定了表达式中运算执行的先后顺序。例如,x>> 无号右移 +例子:int a1 = 8; // 0000 0000 0000 1000 System.out.println(a1>>>2); //// 0000 0000 0000 0010 +输出为 2 +运算符优先级 +按优先级从高到低排列如下:[ ]、 ( )、 ++、--、 !、 ~、 instanceof、 *、 /、 %、 +、 -、《、 》、 >>>、 <>、 < 、=、 >、 \、 ==、 !=、 &、^、& &、 ||、 ? :、= . + +**Java 强制类型转换** +强制和转换 +Java 语言和解释器限制使用强制和转换,以防止出错导致系统崩溃。整数和浮点数运算符间可以来回强制转换,但整数不能强制转换成数组或对象。对象不能被强制为基本类型。 Java 中整数运算符在整数运算时,如果操作数是 long 类型,则运算结果是 long 类型,否则为 int 类型,绝不会是 byte,short 或 char 型。这样,如果变量i被声明为 short 或 byte,i+1 的结果会是 int.如果结果超过该类型的取值范围,则按该类型的最大值取模。 + +**运算符操作** +一、运算符"+",如果必要则自动把操作数转换为 String 型。如果操作数是一个对象,它可定义一个方法 toString()返回该对象的 String 方式,例如 floata=1.0print("Thevalueofais"+a+"\n");+运算符用到的例子 Strings="a="+a;+= 运算符也可以用于 String.注意,左边(下例中的s1)仅求值一次。s1+=a;//s1=s1+a//若 a 非 String型,自动转换为 String 型。 +二、整数算术运算的异常是由于除零或按零取模造成的。它将引发一个算术异常。下溢产生零,上溢导致越界。例如:加1超过整数最大值,取模后,变成最小值。一个 op= 赋值运算符,和上表中的各双目整数运算符联用,构成一个表达式。整数关系运算符<,>,<=,>=,==和!=产生 boolean 类型的数据。 +三、数组运算符数组运算符形式如下:[]可给出数组中某个元素的值。合法的取值范围是从0到数组的长度减1. +四、对象运算符双目运算符 instanceof 测试某个对象是否是指定类或其子类的实例。 +例如:if(myObjectinstanceofMyClass) { MyClassanothermyObject=(MyClass)myObject; … } +是判定 myObject 是否是 MyClass 的实例或是其子类的实例。 +五、浮点运算符浮点运算符可以使用常规运算符的组合:如单目运算符++、--,双目运算符+、-、```*```和/,以及赋值运算符+=,-=,*=,和/=.此外,还有取模运算:%和%=也可以作用于浮点数,例如:a%b 和 a-((int)(a/b)*b)的语义相同。这表示 a%b 的结果是除完后剩下的浮点数部分。只有单精度操作数的浮点表达式按照单精度运算求值,产生单精度结果。如果浮点表达式中含有一个或一个以上的双精度操作数,则按双精度运算,结果是双精度浮点数。 +六、布尔运算符布尔(boolean)变量或表达式的组合运算可以产生新的 boolean 值,fales 和 true(记得是小写)。单目运算符!是布尔非。双目运算符&,|和^是逻辑 AND,OR 和 XOR 运算符,它们强制两个操作数求布尔值。为避免右侧操作数冗余求值,用户可以使用短路求值运算符&&和||. +七、用户可以使用==和!=,赋值运算符也可以用&=、|=、^=.三元条件操作符和 C 语言中的一样。 +八、++运算符用于表示直接加1操作。增量操作也可以用加运算符和赋值操作间接完成。++lvalue(左值表示 lvalue+=1,++lvalue 也表示 lvalue=lvalue+1. +九、--运算符用于表示减1操作。++和--运算符既可以作为前缀运算符,也可以做为后缀运算符。双目整数运算符是:运算符操作**+加-减*乘/除%取模&位与|位或^位异或《左移 》右移(带符号) >>>添零右移整数除法按零舍入。除法和取模遵守以下等式: (a/b)*b+(a%b)==a + +**java 运算符问题:** +&是位 +&&是逻辑 +当&两边是整数时执行的是位运算,而两边是 boolean 值时执行的是逻辑运算, 如: 3&6 就是执行的位运算,结果是一个整数: +2 true&false 执行的就是逻辑运算,结果是一个 boolean 值:false &的逻辑运算和&&逻辑运算是存在一定不同的 +&逻辑运算时两边都会计算的,而&&则在左边为假时则直接返的是 false 不再计算右边 +举个例子: +1:int[] a={1,2,3}; if(a[0]==2&a[3]==4){System.out.println("true")} +2:int[] a={1,2,3}; if(a[0]==2&&a[3]==4){System.out.println("true")} +这两个例子中,第一个会抛出异常,而第二个则什么不会输出也不会抛异常 这是因为第一个例子中 if 语句中用的是&,所以两边的都会计算,当计算a[3]==4时抛出数组下标越界异常。第二个例子则在计算第一个式子a[0]==2发现结果为假则不再计算右边,直接返回 false,所以该例子不会输出任何东西。 \ No newline at end of file diff --git a/virtual-machine.md b/virtual-machine.md new file mode 100644 index 0000000..416b5e5 --- /dev/null +++ b/virtual-machine.md @@ -0,0 +1,147 @@ +# Java 虚拟机 + +# Java 虚拟机工作原理详解 + +一、类加载器 +首先来看一下 java 程序的执行过程。 + +![](images/30.png) + +从这个框图很容易大体上了解 java 程序工作原理。首先,你写好 java 代码,保存到硬盘当中。然后你在命令行中输入 + +``` +javac YourClassName.java +``` + +此时,你的 java 代码就被编译成字节码(.class).如果你是在 Eclipse IDE 或者其他开发工具中,你保存代码的时候,开发工具已经帮你完成了上述的编译工作,因此你可以在对应的目录下看到 class 文件。此时的 class 文件依然是保存在硬盘中,因此,当你在命令行中运行 + +``` +java YourClassName +``` + +就完成了上面红色方框中的工作。JRE 的来加载器从硬盘中读取 class 文件,载入到系统分配给 JVM 的内存区域--运行数据区(Runtime Data Areas). 然后执行引擎解释或者编译类文件,转化成特定 CPU 的机器码,CPU 执行机器码,至此完成整个过程。 + +接下来就重点研究一下类加载器究竟为何物?又是如何工作的? +首先看一下来加载器的一些特点,有点抽象,不过总有帮助的。 + +》》层级结构 +类加载器被组织成一种层级结构关系,也就是父子关系。其中,Bootstrap 是所有类加载器的父亲。如下图所示: + +![](images/31.png) + +--Bootstrap class loader: +当运行 java 虚拟机时,这个类加载器被创建,它加载一些基本的 java API,包括 Object 这个类。需要注意的是,这个类加载器不是用 java 语言写的,而是用 C/C++ 写的。 +--Extension class loader: +这个加载器加载出了基本 API 之外的一些拓展类,包括一些与安全性能相关的类。(目前了解得不是很深,只能笼统说,待日后再详细说明) +--System Class Loader: +它加载应用程序中的类,也就是在你的 classpath 中配置的类。 +--User-Defined Class Loader: +这是开发人员通过拓展 ClassLoader 类定义的自定义加载器,加载程序员定义的一些类。 + +》》委派模式(Delegation Mode) +仔细看上面的层次结构,当 JVM 加载一个类的时候,下层的加载器会将将任务委托给上一层类加载器,上一层加载检查它的命名空间中是否已经加载这个类,如果已经加载,直接使用这个类。如果没有加载,继续往上委托直到顶部。检查完了之后,按照相反的顺序进行加载,如果 Bootstrap 加载器找不到这个类,则往下委托,直到找到类文件。对于某个特定的类加载器来说,一个 Java 类只能被载入一次,也就是说在 Java 虚拟机中,类的完整标识是(classLoader,package,className)。一个雷可以被不同的类加载器加载。 + +![](images/32.png) + +举个具体的例子来说明,现在加入我有一个自己定义的类 MyClass 需要加载,如果不指定的话,一般交 App(System)加载。接到任务后,System 检查自己的库里是否已经有这个类,发现没有之后委托给 Extension,Extension 进行同样的检查,发现还是没有继续往上委托,最顶层的 Boots 发现自己库里也没有,于是根据它的路径(Java 核心类库,如 java.lang)尝试去加载,没找到这个 MaClass 类,于是只好(人家看好你,交给你完成,你无能为力,只好交给别人啦)往下委托给 Extension,Extension 到自己的路径(JAVA_HOME/jre/lib/ext)是找,还是没找到,继续往下,此时 System 加载器到 classpath 路径寻找,找到了,于是加载到 Java 虚拟机。 +现在假设我们将这个类放到 JAVA_HOME/jre/lib/ext 这个路径中去(相当于交给 Extension 加载器加载),按照同样的规则,最后由 Extension 加载器加载 MyClass 类,看到了吧,统一各类被两次加载到 JVM,但是每次都是由不同的 ClassLoader 完成。 + +》》可见性限制 +下层的加载器能够看到上层加载器中的类,反之则不行,也就是是说委托只能从下到上。 + +》》不允许卸载类 +类加载器可以加载一个类,但是它不能卸载一个类。但是类加载器可以被删除或者被创建。 + +当类加载完毕之后,JVM 继续按照下图完成其他工作: + +![](images/33.png) + +框图中各个步骤简单介绍如下: +Loading:文章前面介绍的类加载,将文件系统中的 Class 文件载入到 JVM 内存(运行数据区域) +Verifying:检查载入的类文件是否符合 Java 规范和虚拟机规范。 +Preparing:为这个类分配所需要的内存,确定这个类的属性、方法等所需的数据结构。(Prepare a data structure that assigns the memory required by classes and indicates the fields, methods, and interfaces defined in the class.) +Resolving:将该类常量池中的符号引用都改变为直接引用。(不是很理解) +Initialing:初始化类的局部变量,为静态域赋值,同时执行静态初始化块。 + +那么,Class Loader 在加载类的时候,究竟做了些什么工作呢? +要了解这其中的细节,必须得先详细介绍一下运行数据区域。 + +二、运行数据区域 +Runtime Data Areas:当运行一个 JVM 示例时,系统将分配给它一块内存区域(这块内存区域的大小可以设置的),这一内存区域由 JVM 自己来管理。从这一块内存中分出一块用来存储一些运行数据,例如创建的对象,传递给方法的参数,局部变量,返回值等等。分出来的这一块就称为运行数据区域。运行数据区域可以划分为6大块:Java 栈、程序计数寄存器(PC 寄存器)、本地方法栈(Native Method Stack)、Java 堆、方法区域、运行常量池(Runtime Constant Pool)。运行常量池本应该属于方法区,但是由于其重要性,JVM 规范将其独立出来说明。其中,前面3各区域(PC 寄存器、Java 栈、本地方法栈)是每个线程独自拥有的,后三者则是整个 JVM 实例中的所有线程共有的。这六大块如下图所示: + +![](images/34.png) + +》PC 计数器: +每一个线程都拥有一个 PC 计数器,当线程启动(start)时,PC 计数器被创建,这个计数器存放当前正在被执行的字节码指令(JVM 指令)的地址。 +》Java 栈: +同样的,Java 栈也是每个线程单独拥有,线程启动时创建。这个栈中存放着一系列的栈帧(Stack Frame),JVM 只能进行压入(Push)和弹出(Pop)栈帧这两种操作。每当调用一个方法时,JVM 就往栈里压入一个栈帧,方法结束返回时弹出栈帧。如果方法执行时出现异常,可以调用 printStackTrace 等方法来查看栈的情况。栈的示意图如下: + +![](images/35.png) + +OK。现在我们再来详细看看每一个栈帧中都放着什么东西。从示意图很容易看出,每个栈帧包含三个部分:本地变量数组,操作数栈,方法所属类的常量池引用。 +》局部(本地)变量数组: +局部(本地)变量数组中,从0开始按顺序存放方法所属对象的引用、传递给方法的参数、局部变量。举个例子: + +``` +public void doSomething(int a, double b, Object o) { +... +} +``` + +这个方法的栈帧中的局部变量存储的内容分别是: + +``` +0: this +1: a +2,3:b +4:0 +``` + +看仔细了,其中 double 类型的 b 需要两个连续的索引。取值的时候,取出的是2这个索引中的值。如果是静态方法,则数组第0个不存放 this 引用,而是直接存储传递的参数。 +》操作数栈: +操作数栈中存放方法执行时的一些中间变量,JVM 在执行方法时压入或者弹出这些变量。其实,操作数栈是方法真正工作的地方,执行方法时,局部变量数组与操作数栈根据方法定义进行数据交换。例如,执行以下代码时,操作数栈的情况如下: + +``` +int a = 90; +int b = 10; +int c = a + b; +``` + +![](images/31.jpg) + +注意在这个图中,操作数栈的地步是在上边,所以先压入的100位于上方。可以看出,操作数栈其实是一个数据临时存储区,存放一些中间变量,方法结束了,操作数栈也就没有啦。 +》栈帧中数据引用: +除了局部变量数组和操作数栈之外,栈帧还需要一个常量池的引用。当 JVM 执行到需要常量池的数据时,就是通过这个引用来访问常量池的。栈帧中的数据还要负责处理方法的返回和异常。如果通过 return 返回,则将该方法的栈帧从 Java 栈中弹出。如果方法有返回值,则将返回值压入到调用该方法的方法的操作数栈中。另外,数据区中还保存中该方法可能的异常表的引用。下面的例子用来说明: + +``` +class Example3C{ + public static void addAndPrint(){ + double result = addTwoTypes(1,88.88); + System.out.println(result); + } + public static double addTwoTypes(int i, double d){ + return i+d; + } + +} +``` + +执行上述代码时,Java 栈如下图所示: + +![](images/32.jpg) + +花些时间好好研究上图。一样需要注意的是,栈的底部在上方,先押人员 addAndPrint 方法的栈帧,再压入 addTwoTypes 方法的栈帧。上图最右边的文字说明有错误,应该是 addTwoTypes 的执行结果存放在 addAndPrint 的操作数栈中。 +》》本地方法栈 +当程序通过 JNI(Java Native Interface)调用本地方法(如 C 或者 C++ 代码)时,就根据本地方法的语言类型建立相应的栈。 +》》方法区域 +方法区域是一个 JVM 实例中的所有线程共享的,当启动一个 JVM 实例时,方法区域被创建。它用于存运行放常量池、有关域和方法的信息、静态变量、类和方法的字节码。不同的 JVM 实现方式在实现方法区域的时候会有所区别。Oracle 的 HotSpot 称之为永久区域(Permanent Area)或者永久代(Permanent Generation)。 +》》运行常量池 +这个区域存放类和接口的常量,除此之外,它还存放方法和域的所有引用。当一个方法或者域被引用的时候,JVM 就通过运行常量池中的这些引用来查找方法和域在内存中的的实际地址。 +》》堆(Heap) +堆中存放的是程序创建的对象或者实例。这个区域对 JVM 的性能影响很大。垃圾回收机制处理的正是这一块内存区域。 +所以,类加载器加载其实就是根据编译后的 Class 文件,将 java 字节码载入 JVM 内存,并完成对运行数据处于的初始化工作,供执行引擎执行。 + +三、 执行引擎(Execution Engine) +类加载器将字节码载入内存之后,执行引擎以 Java 字节码指令为但愿,读取 Java 字节码。问题是,现在的 java 字节码机器是读不懂的,因此还必须想办法将字节码转化成平台相关的机器码。这个过程可以由解释器来执行,也可以有即时编译器(JIT Compiler)来完成。 + +![](images/33.jpg) \ No newline at end of file diff --git a/webservice.md b/webservice.md new file mode 100644 index 0000000..a5b8734 --- /dev/null +++ b/webservice.md @@ -0,0 +1,642 @@ +# Webservice + +# Web Service 学习笔记 + +## Web Service 概述 + +### Web Service 的定义 + +W3C 组织对其的定义如下,它是一个软件系统,为了支持跨网络的机器间相互操作交互而设计。Web Service 服务通常被定义为一组模块化的 API,它们可以通过网络进行调用,来执行远程系统的请求服务。 + +这里我们从一个程序员的视角来观察 web service。在传统的程序编码中,存在这各种的函数方法调用。通常,我们知道一个程序模块M中的方法 A,向其发出调用请求,并传入 A 方法需要的参数 P,方法 A 执行完毕后,返回处理结果 R。这种函数或方法调用通常发生在同一台机器上的同一程序语言环境下。现在的我们需要一种能够在不同计算机间的不同语言编写的应用程序系统中,通过网络通讯实现函数和方法调用的能力,而 Web service 正是应这种需求而诞生的。 + +最普遍的一种说法就是,Web Service = SOAP + HTTP + WSDL。其中,SOAP Simple Object Access Protocol)协议是 web service 的主体,它通过 HTTP 或者 SMTP 等应用层协议进行通讯,自身使用 XML 文件来描述程序的函数方法和参数信息,从而完成不同主机的异构系统间的计算服务处理。这里的 WSDL(Web Services Description Language)web 服务描述语言也是一个 XML 文档,它通过 HTTP 向公众发布,公告客户端程序关于某个具体的 Web service 服务的 URL 信息、方法的命名,参数,返回值等。 +下面,我们先来熟悉一下 SOAP 协议,看看它是如何描述程序中的函数方法、参数及结果对象的。 + +## SOAP 协议简介 + +### 什么是 SOAP + +SOAP 指简单对象访问协议,它是一种基于 XML 的消息通讯格式,用于网络上,不同平台,不同语言的应用程序间的通讯。可自定义,易于扩展。一条 SOAP 消息就是一个普通的 XML 文档,包含下列元素: +- Envelope 元素,标识 XML 文档一条 SOAP 消息 +- Header 元素,包含头部信息的 XML 标签 +- Body 元素,包含所有的调用和响应的主体信息的标签 +- Fault 元素,错误信息标签。 + +以上的元素都在 SOAP 的命名空间 http://www.w3.org/2001/12/soap-envelope 中声明; + +SOAP 的语法规则 + +- SOAP 消息必须用 XML 来编码 +- SOAP 消息必须使用 SOAP Envelope 命名空间 +- SOAP 消息必须使用 SOAP Encoding 命名空间 +- SOAP 消息不能包含 DTD 引用 +- SOAP 消息不能包含 XML 处理指令 + +SOAP 消息的基本结构 + +``` + + + + ... + ... + + + ... + ... + + ... + ... + + + +``` + +### SOAP Envelope 元素 + +Envelope 元素是 SOAP 消息的根元素。它指明 XML 文档是一个 SOAP 消息。它的属性 xmlns:soap 的值必须是 http://www.w3.org/2001/12/soap-envelope。 + +encodingStyle 属性,语法:soap:encodingStyle="URI" + +encodingStyle 属性用于定义文档中使用的数据类型。此属性可出现在任何 SOAP 元素中,并会被应用到元素的内容及元素的所有子元素上。 + +``` + + + ... + Message information goes here + ... + +``` + +### SOAP Header 元素 + +- actor 属性,语法 soap:actor="URI" + +通过沿着消息路径经过不同的端点,SOAP 消息可从某个发送者传播到某个接收者。并非 SOAP 消息的所有部分均打算传送到 SOAP 消息的最终端点,不过,另一个方面,也许打算传送给消息路径上的一个或多个端点。SOAP 的 actor 属性可被用于将 Header 元素寻址到一个特定的端点。 + +- mustUnderstand 属性 ,语法 soap:mustUnderstand="0|1" + +SOAP 的 mustUnderstand 属性可用于标识标题项对于要对其进行处理的接收者来说是强制的还是可选的。假如您向 Header 元素的某个子元素添加了 "mustUnderstand="1",则要求处理此头部的接收者必须认可此元素。 + +``` + + + + + + + + Apples + + + +``` + +上面的例子请求苹果的价格。请注意,上面的 m:GetPrice 和 Item 元素是应用程序专用的元素。它们并不是 SOAP 标准的一部分。而对应的 SOAP 响应应该类似这样: + +``` + + + + + 1.90 + + + +``` + +### SOAP Fault 元素 + +Fault 元素表示 SOAP 的错误消息。它必须是 Body 元素的子元素,且在一条 SOAP 消息中,Fault 元素只能出现一次。Fault 元素拥有下列子元素: + +![](images/1.gif) + +常用的 SOAP Fault Codes + +![](images/2.gif) + +### HTTP 协议中的 SOAP 实例 + +下面的例子中,一个 GetStockPrice 请求被发送到了服务器。此请求有一个 StockName 参数,而在响应中则会返回一个 Price 参数。此功能的命名空间被定义在此地址中: "http://www.jsoso.net/stock" + +- SOAP 请求:(注意 HTTP 的 Head 属性) + +``` +POST /InStock HTTP/1.1 +Host: www.jsoso.net +Content-Type: application/soap+xml; charset=utf-8 +Content-Length: XXX + + + + + + IBM + + + +``` + +- SOAP 响应:(注意 HTTP 的 Head 属性) + +``` +HTTP/1.1 200 OK +Content-Type: application/soap+xml; charset=utf-8 +Content-Length: XXX + + + + + + 34.5 + + + +``` + +### HTTP 协议中的 SOAP RPC 工作流程 + +![](images/3.gif) + +## WSDL 简介 + +介绍过了 SOAP,让我们关注 Web Service 中另外一个重要的组成 WSDL。 + +### WSDL 的主要文档元素 + +![](images/4.gif) + +WSDL 文档可以分为两部分。顶部分由抽象定义组成,而底部分则由具体描述组成。抽象部分以独立于平台和语言的方式定义SOAP消息,它们并不包含任何随机器或语言而变的元素。这就定义了一系列服务,截然不同的应用都可以实现。具体部分,如数据的序列化则归入底部分,因为它包含具体的定义。在上述的文档元素中,属于抽象定义层,属于具体定义层。所有的抽象可以是单独存在于别的文件中,也可以从主文档中导入。 + +### WSDL 文档的结构实例解析 + +下面我们将通过一个实际的 WSDL 文档例子来详细说明各标签的作用及关系。 + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +由于上面的事例 XML 较长,我们将其逐段分解讲解 + +### WSDL 文档的根元素:<definitions> + +``` + +…… +…… + +``` + +<definitions>定义了文档中用到的各个 xml 元素的 namespace 缩写,也界定了本文档自己的 targetNamespace="http://www.jsoso.com/wstest",这意味着其它的 XML 要引用当前 XML 中的元素时,要声明这个 namespace。注意 xmlns:tns="http://www.jsoso.com/wstest"这个声明,它标示了使用 tns 这个前缀指向自身的命名空间。 + +引用 +WSDL 文档数据类型定义元素: + +``` + + + + + +``` + +标签定义了当前的WSDL文档用到的数据类型。要说明的是,为了最大程度的平台中立性,WSDL 使用 XML Schema 语法来定义数据类型。这些数据类型用来定义 web service 方法的参数和返回指。对于通用的原生数据类型如:integer , boolean , char , float 等,在 W3C 的标准文档 http://www.w3.org/2001/XMLSchema 中已经做了定义。这里我们要引入的 schema 定义schemaLocation="http://localhost:8080/hello?xsd=1"是我们自定义的 Java 对象类型。 + +### WSDL 文档消息体定义元素:< message > + +``` + + + + + + + + + + + + + + + + + +``` + +<message>元素定义了 web service 函数的参数。<message>元素中的每个<part>子元素都和某个参数相符。输入参数在<message>元素中定义,与输出参数相隔离,输出参数有自己的<message>元素。兼作输入、输出的参数在输入输出的<message>元素中有它们相应的<part>元素。输出<message>元素以"Response"结尾,对Java而言方法得返回值就对应一个输出的<message>。每个<part>元素都有名字和类型属性,就像函数的参数有参数名和参数类型。 + +在上面的文档中有两个输入参数、两个输出参数和一个错误参数(对应Java中的Exception)。 + +输入参数<message>的 name 属性分别命名为 toSayHello,sayHello。 +toSayHello 对应输入参数 userName,参数类型为 xsd:string,在 Java 语言中就是 String; +sayHello 对应两个输入参数 person 和 arg1,类型为 tns:person 和 xsd:string。这里 tns:person 类型就是引用了< types >标签中的类型定义。 + +输出参数<message>的 name 属性分别命名为 toSayHelloResponse 和 sayHelloResponse。 +这个名称和输入参数的<message>标签 name 属性对应,在其后面加上 Response 尾缀。 +toSayHelloResponse 对应的返回值是 returnWord,参数类型为 xsd:string; +sayHelloResponse 对应的返回值是 personList,参数类型为 tns:personArray(自定义类型); + +错误参数<message>的 name 属性为 HelloException。 +它的子标签 element 而不是 type 来定义类型。 + +以上的 message 标签的 name 属性通常使用 web service 函数方法名作为参照,错误参数标签则使用异常类名为参照。标签中的参数名称,即 part 子元素的 name 属性是可自定义的(下一章节详细说明)。message 标签的参数类型将引用 types 标签的定义。 + +### WSDL 文档函数体定义元素:< portType > + +``` + + + + + + + + + + + +``` + + 元素是最重要的 WSDL 元素。它可描述一个 web service、可被执行的操作,以及相关的消息。portType 的 name 属性对应 Java 中的一个服务类的类名。 元素使用其子元素< operation>描述一个 web service 的服务方法。 + +在元素中,name 属性表示服务方法名,parameterOrder 属性表示方法的参数顺序,使用空格符分割多个参数,如:“parameterOrder="person arg1”。元素的子标签表示输入参数说明,它引用<message>标签中的输入参数。表示输出参数说明,它引用<message>标签中的输出参数。标签在 Java 方法中的特别用来表示异常(其它语言有对应的错误处理机制),它引用<message>标签中的错误参数。 + +### WSDL 绑定实现定义元素:< binding > + +``` + + + + + + + + + + + + + + + + + + + + + + + + +``` + +标签是完整描述协议、序列化和编码的地方,,标签处理抽象的数据内容,而标签是处理数据传输的物理实现。 +标签把前三部分的抽象定义具体化。 + +首先标签使用的 transport 和 style 属性定义了 Web Service 的通讯协议 HTTP 和 SOAP 的请求风格 RPC。其次子标签将 portType 中定义的 operation 同 SOAP 的请求绑定,定义了操作名称 soapAction,输出输入参数和异常的编码方式及命名空间。 + +### WSDL 服务地址绑定元素:< service > + +``` + + + + + +``` + +service 是一套<port>元素。在一一对应形式下,每个<port>元素都和一个 location 关联。如果同一个<binding>有多个<port>元素与之关联,可以使用额外的 URL 地址作为替换。 + +一个 WSDL 文档中可以有多个<service>元素,而且多个<service>元素十分有用,其中之一就是可以根据目标URL来组织端口。在一个 WSDL 文档中,<service>的 name 属性用来区分不同的service。在同一个 service 中,不同端口,使用端口的"name"属性区分。 + +这一章节,我们简单的描述了 WSDL 对 SOAP 协议的支持,以及在 Web Service 中的作用。在接下来的章节中,我们将学习如何使用 Java6.0 的 Annotation 标签来定义和生成对应的 WSDL。 + +## JavaSE6.0 下的 Web Service + +从 JavaSE6.0 开始,Java 引入了对 Web Service 的原生支持。我们只需要简单的使用 Java 的 Annotation 标签即可将标准的 Java 方法发布成 Web Service。(PS:Java Annotation 资料请参考 JDK5.0 Annotation 学习笔记(一) ) + +但不是所有的 Java 类都可以发布成 Web Service。Java 类若要成为一个实现了 Web Service 的 bean,它需要遵循下边这些原则: +- 这个类必须是 public 类 +- 这些类不能是 final 的或者 abstract +- 这个类必须有一个公共的默认构造函数 +- 这个类绝对不能有 finalize()方法 + +下面我们将通过一个具体的 Java Web Service 代码例子,配合上述的 WSDL 文件,讲述如何编写 JavaSE6.0 的原生 Web Service 应用。 + +### 完整的 Java Web Service 类代码 + +``` +package org.jsoso.jws.server; + +import java.util.ArrayList; +import javax.jws.WebMethod; +import javax.jws.WebParam; +import javax.jws.WebResult; +import javax.jws.WebService; +import javax.jws.WebParam.Mode; +import javax.jws.soap.SOAPBinding; +/ + * 提供WebService服务的类 + */ +@WebService(name="Example", targetNamespace="http://www.jsoso.com/wstest", serviceName="Example") +@SOAPBinding(style=SOAPBinding.Style.RPC) +public class Example { + private ArrayList persons = new ArrayList();; + /** + * + * 返回一个字符串 + * @param userName + * @return + */ + @WebMethod(operationName="toSayHello",action="sayHello",exclude=false) + @WebResult(name="returnWord")//自定义该方法返回值在WSDL中相关的描述 + public String sayHello(@WebParam(name="userName")String userName) { + return "Hello:" + userName; + } + + /** + * web services 方法的返回值与参数的类型不能为接口 + * @param person + * @return + * @throws HelloException + */ + @WebMethod(operationName="sayHello", action="sayHello") + @WebResult(partName="personList") + public Person[] sayHello(@WebParam(partName="person", mode=Mode.IN)Person person, + String userName) throws HelloException { + if (person == null || person.getName() == null) { + throw new HelloException("说hello出错,对像为空。。"); + } + System.out.println(person.getName() + " 对 " + userName + " 说:Hello,我今年" + person.getAge() + "岁"); + persons.add(person); + return persons.toArray(new Person[0]); + } +} +``` + +**Annotation 1@WebService(name="Example", targetNamespace="http://www.jsoso.com/wstest", serviceName="Example") ** +@WebService 标签主要将类暴露为 WebService,其中 targetNamespace 属性定义了自己的命名空间,serviceName 则定义了< definitions >标签和标签的 name 属性。 + +**Annotation 2:@SOAPBinding(style=SOAPBinding.Style.RPC) ** +@SOAPBinding 标签定义了 WSDL 文档中 SOAP 的消息协议,其中 style 属性对应 SOAP 的文档类型,可选的有 RPC 和 DOCUMENT + +**Annotation 3:@WebMethod(operationName="toSayHello",action="sayHello",exclude=false)** +@WebMethod 定义 Web Service 运作的方法, +属性 action 对应操作的活动 ,如 +属性 operationName 匹配的 wsdl:operation 的名称,如 +属性 exclude 用于阻止将某一继承方法公开为 web 服务,默认为 false + +**Annotation 4:@WebResult(name="returnWord") ** +@ WebResult定 义方法返回值得名称,如 + +**Annotation 5:@WebParam(partName="person", mode=Mode.IN ** +@WebParam 定义方法的参数名称,如,其中 mode 属性表示参数的流向,可选值有 IN / OUT / INOUT + +这里要着重说明的是,上述 Web Service 类的 sayHello 方法中,带有 HelloException 这个异常声明,造成该服务类不能直接发布成 Web Service。需要使用 wsgen 工具为其生存异常 Bean。关于wsgen 工具的使用,请参考 wsgen 与 wsimport 命令说明 + +**发布一个的 Java Web Service ** + +在完成了上述的 Web Service Annotation 注释后,我们使用 wsgen 工具为其进行服务资源文件的构造(这里主要是生成一个名为 org.jsoso.jws.server.jaxws.HelloExceptionBean 的异常 bean 类),最后使用以下的类发布 Web 服务: + +``` +package org.jsoso.jws.server; + +import java.util.LinkedList; +import java.util.List; +import javax.xml.ws.Binding; +import javax.xml.ws.Endpoint; +import javax.xml.ws.handler.Handler; + +/** + * @author zsy 启动web services服务 + */ +public class StartServer { + + /** + * @param args + */ + public static void main(String[] args) { + /* + * 生成Example 服务实例 + */ + Example serverBean = new Example(); + /* + * 发布Web Service到http://localhost:8080/hello地址 + */ + Endpoint endpoint = + Endpoint.publish("http://localhost:8080/hello", serverBean); + Binding binding = endpoint.getBinding(); + /* + * 设置一个SOAP协议处理栈 + * 这里就简单得打印SOAP的消息文本 + */ + List handlerChain = new LinkedList(); + handlerChain.add(new TraceHandler()); + binding.setHandlerChain(handlerChain); + System.out.println("服务已启动 http://localhost:8080/hello"); + } +} +``` + +在控制台运行这个类,就可以使用 URL :http://localhost:8080/hello?wsdl 浏览到上文所描述的 WSDL 的全文了。这说明您的第一个 Web Service 应用发布成功! + +### 构建 Web Service 客户端 + +使用 JavaSE6.0 构建 Web Service 的客户端是一件相当简单的事。这里我们要使用到 JDK 中的另一个命令行工具 wsimport。在控制台下输入以下命令: +引用 +wsimport -d ./bin -s ./src -p org.jsoso.jws.client.ref http://localhost:8080/hello?wsdl + +即可在包 org.jsoso.jws.client.ref 中生成客户端的存根及框架文件。其中我们要使用的类只有两个:服务类 Example_Service 和本地接口 Example。编写如下客户端,即可调用 Web Service 服务: + +``` +package org.jsoso.jws.client; + +import org.jsoso.jws.client.ref.*; + +public class RunClient { + + /** + * @param args + */ + public static void main(String[] args) { + //初始化服务框架类 + Example_Service service = new Example_Service(); + //或者本地服务借口的实例 + Example server = (Example) service.getExamplePort(); + try { + //调用web service的toSayHello方法 + System.out.println("输入toSayHello的返回值——" + server.toSayHello("阿土")); + Person person = new Person(); + person.setName("阿土"); + person.setAge(25); + //调用web service的sayHello方法 + server.sayHello(person, "机器人"); + + person = new Person(); + person.setName("aten"); + person.setAge(30); + //调用web service的sayHello方法 + PersonArray list = server.sayHello(person, "机器人"); + //输出返回值 + System.out.println("/n以下输入sayHello的返回值——"); + for (Person p : list.getItem()) { + System.out.println(p.getName() + ":" + p.getAge()); + } + } catch (HelloException_Exception e) { + e.printStackTrace(); + } + } +} +``` + +届此,本次 Web Service 的学习暂告一个段落。Java Web Service 是一个相当庞大的知识体系,其中涉及的相关技术较多,这里无法一一道来,我们将会在今后的开发和使用中,同大家做进一步深入的探讨和学习。 + +## 附录:wsgen 与 wsimport 命令说明 + +### wsgen +wsgen 是在 JDK 的 bin 目录下的一个 exe 文件(Windows 版),该命令的主要功能是用来生成合适的 JAX-WS。它读取 Web Service 的终端类文件,同时生成所有用于发布 Web Service 所依赖的源代码文件和经过编译过的二进制类文件。这里要特别说明的是,通常在 Web Service Bean 中用到的异常类会另外生成一个描述 Bean,如果 Web Service Bean 中的方法有申明抛出异常,这一步是必需的,否则服务器无法绑定该对像。此外,wsgen 还能辅助生成 WSDL 和相关的 xsd 文件。wsgen 从资源文件生成一个完整的操作列表并验证 web service 是否合法,可以完整发布。 +### 命令参数说明: +- -cp 定义 classpath +- -r 生成 bean的wsdl 文件的存放目录 +- -s 生成发布 Web Service 的源代码文件的存放目录(如果方法有抛出异常,则会生成该异常的描述类源文件) +- -d 生成发布 Web Service 的编译过的二进制类文件的存放目录(该异常的描述类的 class 文件) + +### 命令范例:wsgen -cp ./bin -r ./wsdl -s ./src -d ./bin -wsdl org.jsoso.jws.server.Example + +### wsimport +wsimport 也是在 JDK 的 bin 目录下的一个 exe 文件(Windows 版),主要功能是根据服务端发布的 wsdl 文件生成客户端存根及框架,负责与 Web Service 服务器通信,并在将其封装成实例,客户端可以直接使用,就像使用本地实例一样。对 Java 而言,wsimport 帮助程序员生存调用 web service 所需要的客户端类文件.java 和.class。要提醒指出的是,wsimport 可以用于非 Java 的服务器端,如:服务器端也许是C#编写的web service,通过wsimport则生成Java的客户端实现。 +### 命令参数说明: +- -d 生成客户端执行类的 class 文件的存放目录 +- -s 生成客户端执行类的源文件的存放目录 +- -p 定义生成类的包名 + +### 命令范例:wsimport -d ./bin -s ./src -p org.jsoso.jws.client.ref http://localhost:8080/hello?wsdl \ No newline at end of file