??xml version="1.0" encoding="utf-8" standalone="yes"?>
自从Java 1.0版本首次受到开发h员欢q以来,Java语言的语法就没有发生q太大的变化。虽?.1版本增加了内部类和匿名内部类Q?.4版本增加了带有新的assert关键字的assertionQ断定)功能Q但Java语法和关键字仍然保持不变--像编译时帔R一样处于静态。它通过J2SE 1.5Q代号TigerQ发生改变?/span>
q去的J2SE版本主要x新类和性能Q而Tiger的目标则是通过使Java~程更易于理解、对开发h员更为友好、更安全来增强Java语言本nQ同时最大限度地降低与现有程序的不兼Ҏ。该语言中的变化包括genericsQ泛化)、autoboxing、一个增强的“for”@环?typesafe enumsQ类型安全的枚DcdQ、一个静态导入工Pstatic import facilityQ和varargs?/span>
通过generics来改q类型检?/span>
generics使你能够指定一个集合中使用的对象的实际cdQ而不是像q去那样只是使用Object。generics也被UCؓ“参数化cd”,因ؓ在generics中,一个类的类型接受媄响其行ؓ的类型变量?/span>
genericsq不是一个新概念。C++中有模板Q但是模杉K常复杂ƈ且会D代码膨胀。C++~码人员能够仅用C++模板Q通过一些小的技巧来执行阶乘函数Q然后看着~译器生成C++源代码来处理模板调用。Java开发h员已l从C++语言中学C很多关于generics的知识,q经q了_长时间的实践Q知道如何正用它们。Tiger的当前计划是从健壮的Generic Java (GJ)Ҏ演变而来的。GJҎ的口h“Java的类型简化、再化。?/span>
Z了解genericsQ让我们从一个不使用generics的例子开始。下面这D代码以写字母打印了一个字W串集合Q?/span>
//获得一个字W串集合 public void lowerCase(Collection c) { Iterator itr = c.iterator(); while (itr.hasNext()) { String s = (String) itr.next(); System.out.println(s.toLowerCase()); } }
q个Ҏ不保证只接收字符丌Ӏ编Eh员负责记住传l这个方法什么类型的变量。Generics通过昑ּ声明cd来解册个问题。Generics证明q执行了关于集合包含什么东西的规则。如果类型不正确Q编译器׃产生一个错误。在下面的改写代码中Q注意Collection和Iterator是如何声明它们只接收字符串对象的Q?/span>
public void lowerCase( Collection<String> c) { Iterator<String> itr = c.iterator(); while (itr.hasNext()) { System.out.println( itr.next().toLowerCase()); } }
现在Q该代码包含了更强大的类型,但它仍然包含许多键盘cd。我们将在后面加以介l。注意,你可以存储类型参数的M子类型。接下来Q我们将使用q个Ҏdraw()一个Ş犉合?/span>
// 获得孩子集合... public void drawAll(Collection<Shape> c) { Iterator<Shape> itr = c.iterator(); while (itr.hasNext()) { itr.next().draw(); } }
括号中的DUCؓcd变量。参数化cd能够支持M数量的类型变量。例如,java.util.Map支持两个类型变?-一个用于键cdQ一个用于值类型。下面的例子使用了一个带有指向一列元素对象的字符串查N的mapQ?/span>
public static void main(String[] args) { HashMap<String, List<Element>> map = new HashMap<String, List<Element>>(); map.put("root", new ArrayList<Element>()); map.put("servlet", new LinkedList<Element>()); }
q个cd义声明了它支持多个cd变量。类型参数的数量必须_C所期望的相匚w。而且Q类型变量一定不能是原始cdQprimitive typesQ?/span>
List<String, String> // takes one List<int> // 无效的,原始cd
即在期望用一个普通类型(raw typeQ的时候,你也可以使用一个参数化cd。当Ӟ你也可以反过来做Q但q么做会收到一条编译时警告Q?/span>
public static void oldMethod(List list) { System.out.println(list.size()); } public static void main(String[] args) { List<String> words = new ArrayList<String>(); oldMethod(words); // 没问? }
q就实现了轻杄向后兼容Q接受一个原始列表的老方法能够直接接受一个参数化List<String>。接受参数化List<String>的新Ҏ也能够接受一个原始列表,但是因ؓ原始列表不声明或执行相同的类型约束,所以这个动作会触发一个警告。可以保证的是:如果在编译时你没有得到名为uncheckedQ未查)的警告,那么在运行时~译器生成的强制cd转换QcastQ将不会p|?/span>
有趣的是Q参数化cd和普通类型被~译为相同的cd。没有专门的cL指定q一点,使用~译器技巧就可以完成q一切。instanceof查可以证明这一炏V?/span>
words instanceof List // true words instanceof ArrayList //true words instanceof ArrayList<String> // true
q个查生了一个问题:“如果它们是相同的类型,q种查能起多大作用?”这是一条用墨水而不是用血写的U束。这D代码将产生一个编译错误,因ؓ你不能向List<String>中添加新的PointQ?/span>
List<String> list = new ArrayList<String>(); list.add(new Point()); // ~译错误
但是q段代码被编译了Q?/span>
List<String> list = new ArrayList<String>(); ((List)list).add(new Point());
它将参数化类型强制{换ؓ一个普通类型,q个普通类型是合法的,避免了类型检查,但正如前面所解释的那P却生了一个调用未查的警告Q?/span>
warning: unchecked call to add(E) as a member of the raw type java.util.List ((List)list).add(new Point()); ^
写一个参数化cd
Tiger提供了一个写参数化类型的新语法。下面显C的Holdercd以存放Q意引用类型。这Lcd便于使用Q例如,通过引用语义支持CORBA传递,而不需要生成单独的Holderc:
public class Holder<A> { private A value; Holder(A v) { value = v; } A get() { return value; } void set(A v) { value = v; } }
使用一个参数化的HoldercdQ你能够安全地得到和讄数据Q而不需q行强制cd转换Q?/span>
public static void main(String[] args) { Holder<String> holder = new Holder<String>("abc"); String val = holder.get(); // "abc" holder.set("def"); }
“A”类型参数名可以是Q何标准的变量名。它通常是一个单一的大写字母。你也可以声明类型参数必能够扩展另一个类Q如下所C:
// 也可? public class Holder<C extends Child>
关于是否能够声明M其他的类型参C然存在争议。你对generics了解的越深,你需要的Ҏ规则p多,但是Ҏ规则多Qgenerics׃复杂?/span>
Tiger中设计用来保存线E局部变量(thread local variableQ的核心cjava.lang.ThreadLocalQ将可能变得与下面这?Holdercȝ作用cMQ?
public class ThreadLocal<T> { public T get(); public void set(T value); }
我们也将看见java.lang.Comparable的变化,允许cd明与它们相比较的cdQ?
public interface Comparable<T> { int compareTo(T o); } public final class String implements Comparable<String> { int compareTo(String anotherString); }
Generics不仅仅用于集合,它们有更为广泛的用途。例如,虽然你不能基于参数化cdQ因为它们与普通类型没有什么不同)q行捕捉QcatchQ,但是你可以抛?throw)一个参数化cd。换句话_你可以动态地军_throws语句中抛Z么?/span>
下面q段令h思维混ؕ的代码来自generics规范。该代码通过扩展Exception的类型参数E定义了一?Action接口。ActioncL一个抛Z为E 出现的Q何类型的run()Ҏ。然后,AccessControllercd义一个接受Action<E>的静态exec()ҎQƈ声明exec()抛出E。声明该Ҏ自n是参数化的需要该Ҏ标记Qmethod signatureQ中的特D?lt;E extends Exception>?/span>
现在Q事情变得有Ҏ手了。main()Ҏ调用在Action 实例Q作Z个匿名内部类实现Q中传递的AccessController.exec()Ҏ。该内部c被参数化,以抛Z?FileNotFoundException。main()Ҏ有一个捕捉这一异常cd的catch语句。如果没有参数化cdQ你不能确切地知道run()会抛Z么。有了参数化cdQ你能够实现一个泛化的Actionc,其中run()Ҏ可以L实现Qƈ可以抛出L异常QExceptionQ:
interface Action<E extends Exception> { void run() throws E; } class AccessController { public static <E extends Exception> void exec(Action<E> action) throws E { action.run(); } } public class Main { public static void main(String[] args) { try { AccessController.exec( new Action<FileNotFoundException>() { public void run() throws FileNotFoundException { // someFile.delete(); } }); } catch (FileNotFoundException f) { } } }
协变q回cd
下面q行一个随堂测验:下面的代码是否能够成功编译?
class Fruit implements Cloneable { Fruit copy() throws CloneNotSupportedException { return (Fruit)clone(); } } class Apple extends Fruit implements Cloneable { Apple copy() throws CloneNotSupportedException { return (Apple)clone(); } }
{案Q该代码在J2SE 1.4中不能编译,因ؓ改写一个方法必L相同的方法标讎ͼ包括q回cdQ作为它改写的方法。然而,generics有一个叫做协变返回类型的Ҏ,使上面的代码能够在Tiger中进行编译。该Ҏ是极ؓ有用的?
例如Q在最新的JDOM代码中,有一个新的Child接口。Child有一个detach()ҎQ返回从其父对象分离的Child对象。在Child接口中,该方法当然返回ChildQ?
public interface Child { Child detach(); // etc }
当Commentcd现detach()Ӟ它Lq回一个CommentQ但如果没有协变q回cdQ该Ҏ声明必须q回ChildQ?/span>
public class Comment { Child detach() { if (parent != null) parent.removeContent(this); return this; } }
q意味着调用者一定不要将q回的类型再向下q回C个Comment。协变返回类型允许Comment 中的detach()q回 Comment。只要Comment是Child的子cd行。除了能够返回Document的DocType和能够返回Element的EntityRefQ该Ҏ对立刻q回Parent 的Child.getParent()Ҏ也能z上用场。协变返回类型将定q回cd的责Mcȝ用户Q通过强制cd转换认Q{交给cȝ创徏者,只有创徏者知道哪些类型彼此之间是真正多态的?q应用~程接口(API)的用户用v来更ҎQ但却稍微增加了API设计者的负担?/span>
Autoboxing
Java有一个带有原始类型和对象Q引用)cd的分割类型系l。原始类型被认ؓ是更M的,因ؓ它们没有对象开销。例如,int[1024]只需?K存储I间Q以及用于数l自w的一个对象。然而,引用cd能够在不允许有原始类型的地方被传递,例如Q传递到一个List。这一限制的标准工作场景是在诸如list.add(new Integer(1))的插入操作之前,原始类型与其相应的引用cd装Qbox或wrapQ在一P然后用诸?(Integer)list.get(0)).intValue()的方法取出(unboxQ返回倹{?/span>
新的 autoboxingҎɾ~译器能够根据需要隐式地从int转换为IntegerQ从char 转换为Character{等。auto-unboxingq行相反的操作。在下面的例子中Q我不用autoboxing计算一个字W串中的字符频率。我构造了一个应该将字符型映ؓ整型的MapQ但是由于Java的分割类型系l,我不得不手动理Character和Integer箱转换Qboxing conversionsQ?/span>
public static void countOld(String s) { TreeMap m = new TreeMap(); char[] chars = s.toCharArray(); for (int i=0; i < chars.length; i++) { Character c = new Character(chars[i]); Integer val = (Integer) m.get(c); if (val == null) val = new Integer(1); else val = new Integer(val.intValue()+1); m.put(c, val); } System.out.println(m); }
Autoboxing使我们能够编写如下代码:
public static void countNew(String s) { TreeMap<Character, Integer> m = new TreeMap<Character, Integer>(); char[] chars = s.toCharArray(); for (int i=0; i < chars.length; i++) { char c = chars[i]; m.put(c, m.get(c) + 1); // unbox } System.out.println(m); }
q里Q我重写了mapQ以使用genericsQ而且我让autoboxingl出了map能够直接存储和检索char 和int倹{不q的是,上面的代码有一个问题。如果m.get(c)q回I|nullQ,会发生什么情况呢Q怎样取出null|在抢鲜版Qearly access releaseQ?参见下一?中,取出一个空的Integer 会返?。自抢鲜版vQ专家组军_取出null值应该抛Z个NullPointerException。因此,put()Ҏ需要被重写Q如下所C:
m.put(c, Collections.getWithDefault( m, c) + 1);
新的Collections.getWithDefault()Ҏ执行get()函数Q在该方法中Q如果gؓI|它将q回期望cd的默认倹{对于一个intcd来说Q则q回0?/span>
虽然autoboxing有助于编写更好的代码Q但我的是}慎地使用它。封p{换仍然会q行q仍然会创徏许多包装对象Qwrapper-objectQ实例。当q行计数Ӟ采用一个int与一个长度ؓ1的的 int数组装在一L旧方法更好。然后,你可以将该数l存储在M需要引用类型的地方Q获取intarr[0]的值ƈ使用intarr[0]++递增。你甚至不必再次调用put()Q因Z在适当的位|生增量。用这一Ҏ和其他一些方法,你能够更有效地进行计数。用下面的法Q执?00万个字符的对旉会从650毫秒~短?0毫秒Q?/span>
public static void countFast(String s) { int[] counts = new int[256]; char[] chars = s.toCharArray(); for (int i=0; i < chars.length; i++) { int c = (int) chars[i]; counts[c]++; // no object creation } for (int i = 0; i < 256; i++) { if (counts[i] > 0) { System.out.println((char)i + ":" + counts[i]); } } }
在C#中,我们可以看到一个类gE微不同的方法。C#有一个统一的类型系l,在这个系l中值类型和引用cd都扩展System.Object。但是,你不能直接看到这一点,因ؓC#为简单的值类型提供了别名和优化。int是System.Int32的一个别名,short是System.Int16的一个别名,double是System.Double的一个别名。在C#中,你能够调用“int i = 5Q?i.ToString()Q”,它是完全合法的。这是因为每个值类型都有一个在它被转换为引用类型时创徏的相应隐藏引用类型(在值类型被转换Z个引用类型时创徏的)?
int x = 9; object o = x; //创徏了引用类? int y = (int) o;
当基于一个不同的cdpȝӞ最l结果与我们在J2SE 1.5中看到的非常接近?/span>
对于循环的增?/span>
q记得前面的q个例子么?
public void drawAll(Collection<Shape> c) { Iterator<Shape> itr = c.iterator(); while (itr.hasNext()) { itr.next().draw(); } }
你再也不用输入这么多的文字了Q这里是Tiger版本中的新格式?/span>
public void drawAll(Collection<Shape> c) { for (Shape s : c) { s.draw(); } }
你可以阅读这样一D代码“foreach Shape s in c”。我们注意到设计者非常聪明地避免dM新的关键字。考虑到很多h都用“in”来输入数据,我们Ҏ应该感到非常高兴。编译器该新的语法自动扩展到其q代表中?
for (Iterator<Shape> $i = c.iterator(); $i.hasNext(); ) { Shape s = $i.next(); s.draw(); }
你可以用该语法来对普通(rawQ非参数化的Q类型进行P代,但是~译器会输出一个警告,告诉你必ȝcd转换可能会失败。你可以在Q何数l和对象上用“foreach”来实现新的接口java.lang.Iterable?/span>
public interface Iterable<T> { SimpleIterator<T> iterator(); } public interface SimpleIterator<T> { boolean hasNext(); T next(); }
java.lang中的新的接口避免对java.util的Q何语a依赖性。Java语言在java.lang之外必须没有依赖性。要注意通过next()Ҏ来更巧妙C用协变返回类型。需要说明的一ҎQ利用该“foreach”语法和SimpleIterator接口Q就会p用iterator.remove()的能力。如果你q需要该能力,则必M自己q代该集合?/span>
与C#Ҏ一下,我们会看到相似的语法Q但是C#使用“foreach”和“in”关键字Q从最初版本开始它们就被作Z留字?/span>
// C# foreach (Color c in colors) { Console.WriteLine(c); }
C#的“foreach”对M集合QcollectionQ或者数l以及Q何可列D的实现都有效。我们再一ơ看C在Java和C#之间的非常相g处?/span>
Q类型安全的枚DcdQtypesafe enum
enums 是定义具有某些命名的帔R值的cd的一U方式。你在CQC++中已l见q它们,但是昄Q它们曾l在Java中不用。现在,l过了八q之后,Java重又采用它们Qƈ且大概比先前的Q何语a都用得更好。让我们首先来看看先前我们是如何解决enum问题的?不知道你有没有编写过如下代码Q?
class PlayingCard { public static final int SUIT_CLUBS = 0; public static final int SUIT_DIAMONDS = 1; public static final int SUIT_HEARTS = 2; public static final int SUIT_SPADES = 3; // ... }
q段代码很简单也很常见,但是它有问题。首先,它不是类型安全的QtypesafeQ。可以给一个方法传递文字?”来获取一个suitQƈ且将被编译。同Ӟq些值用q些帔R直接被编译成每个cRJava通过q些帔Rq行q种"内联"QinliningQ来辑ֈ优化的目的,但其风险在于Q如果对q些值重新排序ƈ且只重新~译该类Q则其他cd会错误地处理q些suits?而且Q该cd是非常原始的Q它不能被扩展或者增强,同时如果你输些g的一个,你只会得C个含意模p的整型量,而不是一个好记的有用名字。这U方法非常简单,q也正是我们Z么要q样做的原因Q但是它q不是最好的Ҏ。所以,也许需要尝试一下下面的q个ҎQ?/span>
class PlayingCard { class Suit { public static final Suit CLUBS = new Suit(); public static final Suit DIAMONDS = new Suit(); public static final Suit HEARTS = new Suit(); public static final Suit SPADES = new Suit(); protected Suit() { } } }
它是cd安全的(typesafeQ且更加h可扩展性,q且Q属于面向对象设计的cR然而,q样单的一U方法ƈ不支持序列化Q没有合法值的列表Q无法将q些值排序,q且Q不能作Z个有意义的字W串来打C个倹{你当然可以dq些Ҏ,Josh Bloch在他的Effective Java一书中Q第五章Q第21条)为我们准展CZ如何解决q些问题。然而,你最l得到的是几蹩脚的代码?/span>
Java新的enumҎ具有一个简单的单行语法Q?
class PlayingCard { public enum Suit { clubs, diamonds, hearts, spades } }
被称之ؓenumQ枚举)cd的该suitQ对应于每个enum帔R都有一个成员项。每个enumcd都是一个实际类Q它可以自动扩展新类java.lang.Enum。编译器赋予enumcM有意义的String()?hashCode(), 和equals() Ҏ, q且自动提供SerializableQ可序列化的Q和ComparableQ可比较的)能力。o人高兴地是enumcȝ声明是递归型的Q?/span>
public class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
使用最新的enumcd可以提供很多好处Q包括:比整型操作(int operationsQ更好的性能Q编译时更好的类型安全性,不会被编译到客户端ƈ且可以被重新命名和排序的帔RQ打印的值具有含意清晰的信息Q能够在集合QcollectionsQ甚至switch中被使用Q具有添加域QfieldsQ和Ҏ的能力,以及实现L接口的能力?
每个enumh一个字W串名字和一个整型顺序号|
out.println(Suit.clubs); // "clubs" out.println(Suit.clubs.name); // "clubs" out.println(Suit.clubs.ordinal); // 0 out.println(Suit.diamonds.ordinal); // 1 Suit.clubs == Suit.clubs // true Suit.clubs == Suit.diamonds // false Suit.clubs.compareTo(Suit.diamonds) // -1
enum可以拥有构造器和方法。甚至一个main()Ҏ都是合法的。下面的例子Dl罗马数字:
public enum Roman { I(1), V(5), X(10), L(50), C(100), D(500), M(1000); private final int value; Roman(int value) { this.value = value; } public int value() { return value; } public static void main(String[] args) { System.out.println(Roman.I); } }
非常奇怪的是,不能序列数Dl一个enumQ比如说“enum Month{jan=1,feb=2,?}”?然而,却可以给enum帔Rd行ؓ。比如说Q在JDOM中,XMLOutputter支持数种I白处理Ҏ。如果JDOM是参照J2SE1.5构徏的,那么q些Ҏ可以用一个enum来定义,q且enumcd本n可以hq种处理行ؓ。不这U编码模式是不是会被证明是有用的Q我们都会逐渐了解它。肯定这是一个异常有的概念?
public abstract enum Whitespace { raw { String handle(String s) { return s; } }, trim { String handle(String s) { return s.trim(); } }, trimFullWhite { String handle(String s) { return s.trim().equals("") ? "":s; } }; abstract String handle(String s); public static void main(String[] args) { String sample = " Test string "; for (Whitespace w : Whitespace.VALUES) System.out.println(w + ": '" + w.handle(sample) + "'"); }}
很少有公开的出版物谈及Java新的enum。enum帔R名是不是都应该都大写Q对于常量来_q是一个标准,但是规范指出写名称“于更好的字W串格式Q一般应该避免用定制的toStringҎ。”另外,名字和顺序号该是域还是方法?q是装Ҏ一再引起争论的问题。在向J2SE1.5d关键字方面,该特性也落了一个不太好的名声。oZ心的是,它还是一个通常被用作存储EnumeratorQ计数器Q实例的词。如果你已经在你的代码中使用了“enum”,那么在你为J2SE 1.5的应用编译之前,必须修改它。现在,你已得到了充分的警示?
让我们看一下C#Q所有的enum都扩展成System.Enum。每个enum都具有可以被赋值的整型Q或者字节型或者其他类型)倹{enumq拥有静态方法,以便从字W串帔R来初始化enumQ获取有效值列表,从而,可以看到某个值是不是被支持。通过使用[flags]属性来标记一个enumQ你可以保值支持位屏蔽Qƈ且系l负责打印被屏蔽的值的有用输出:
// C# [Flags] public enum Credit : byte { Visa = 1, MC = 2, Discover = 4 } Credit accepted = Credit.Visa | Credit.MC; c.WriteLine(accepted); // 3 c.WriteLine(accepted.Format());//"Visa|MC"
静态导?/span>
静态导入得我们可以将一套静态方法和域放入作用域QscopeQ。它是关于调用的一U羃写,可以忽略有效的类名。比如说Q对Math.abs(x)的调用可以被单地写成 abs(x)。ؓ了静态地导入所有的静态域和方法,我们可以使用“import static java.lang.Math”,或者指定要导入的具体内容,而用“import static java.lang.System.out?-在这里没有什么o人激动的新特性,只是~写而已。它让你可以不用Math而来完成mathQ数字计)?
import static java.lang.Math.*; import static java.lang.System.out; public class Test { public static void main(String[] args) { out.println(abs(-1) * PI); } }
注意Q“static”关键字的重用是Z避免M新的关键词。语a成熟,对于“static”关键词的用就多。如果在静态成员之间发生冲H的话,׃出现含的编译错误,q一点跟cȝ导入一栗是否将java.lang.Math.* 作ؓ固有的导入引发了一定的争论Q不q在L其将会触发含L错误之后Q这U争Z会再发生了?/span>
Varargs
“varargs”表C“参数的变量”,存在于C语言中,q且支持通用的printf()和scanf()函数?在Java中,我们通过~写一些接受Object[]、List、PropertiesQ属性)的方法以及可以描q多个值的其它单数据结?-比如_Method.invokeQObject objQObject[] args--来模拟这一Ҏ。)。这要求调用E序数据封装到q种单一的容器结构中。varargs允许调用E序传递值的L列表Q而编译器会ؓ接收E序其转化为数l。其语法是在参数声明中的参数名之后d?..”,以便使其成ؓvararg。它必须是最后一个参?-比如_~写一个sum()函数以便Q意数量的整数相加Q?
out.println(sum(1, 2, 3)); public static int sum(int args...) { int sum = 0; for (int x : args) { sum += x; } return sum; }
在抢鲜版本中QvarargW号使用ҎP像sum(int[] args...)。然而,在之后的讨论中,ҎJames Gosling的提议,Ҏ可L了。在q里的例子中Q我们不使用ҎP但是如果你需要在抢鲜版本中用这些代码的话,需要将Ҏh加上厅R借助autoboxingQ可以通过接受一个Object args…,可以接受Mcd的参敎ͼ包括原始cd。这与printf()cd的一些方法一P它们接受M数量的所有类型的参数。实际上Q该Tiger版本可以使用q一Ҏ通过formatҎQ其行ؓ与printf()一P来提供一个FormattableQ可格式化)的接口。这是我们在以后的文章中要讨论的话题。目前,我们只编写简单的printf()Q?/span>
public static void printf(String fmt,
int args...) {
int i = 0;
for (char c : fmt.toCharArray()) {
out.print(c == '%' ? args[i++] : c);
}
}
public static void main(String[] args) {
printf("My values are % and %
",
1, 2);
}
在Tiger版本中,你会发现采用一个新格式的invokeQ)函数Q?invoke(Object obj, Object args...). q看上去更加自然?/span>l论
J2SE1.5版努力Java的编E更加简ѝ安全和更加富有表现力。这些特性和谐完的被结合在一赗如果你跟我一PL喜欢用老的“for”@环,你肯定希
望你拥有Tiger?/span>然而,需要记住的是,该规范ƈ没有最l完成,很多地方q需要修攏V管理这些变化的专家组QJSR-14,JSR-175以及JSR-201Q会?003q?br />q末的beta版本发布之前Q以及预期在2004q发布最l版发布之前Q会做出很多修改。然而,Sun表达了对JavaOne的信心,认ؓM上主要原则不会改变太多?br />如果你想体验一下,那么你可以从下面的站点获取抢鲜版本?从中你会扑ֈ可能从Q何一个预览版软g都会遇到的错误,但是也会看到很多Ȁ动h心的新特性?br />我强烈徏议你试一下?/span>
关键?/font> Java; 内存泄漏; GC(垃圾攉? 引用; Optimizeit
问题的提?/font>
W者曾l参与开发的|管pȝQ系l规模庞大,涉及上百万行代码。系l主要采用Java语言开发,大体上分为客L、服务器和数据库三个层次。在版本q入试和试用的q程中,现场人员和测试部人员UL反映:pȝ的稳定性比较差Q经怼出现服务器端q行一昼夜死机的现象Q客L跑死的现象也比较频繁地发生。对于网系l来Ԍl常性的服务器死机是个比较严重的问题Q因为频J的L不仅可能D前后台数据不一_发生错误Q更会引LL不满Q降低客L信Q度。因此,服务器端的稳定性问题必d快解冟?/p>
解决思\
通过察看服务器端日志Q发现死机前服务器端频繁抛出OutOfMemoryException内存溢出错误Q因此初步把L的原因定位ؓ内存泄漏引v内存不Q进而引起内存溢出错误。如何查扑ּ起内存泄漏的原因?有两U思\:W一U,安排有经验的~程人员对代码进行走查和分析Q找出内存泄漏发生的位置;W二U,使用专门的内存泄漏测试工具Optimizeitq行试。这两种Ҏ都是解决pȝE_性问题的有效手段Q用内存测试工具对于已l暴露出来的内存泄漏问题的定位和解决非常有效;但是软g试的理Z告诉我们Q系l中永远存在一些没有暴露出来的问题Q而且Q系l的E_性问题也不仅仅只是内存泄漏的问题Q代码走查是提高pȝ的整体代码质量乃臌x在问题的有效手段。基于这L考虑Q我们的内存E_性工作决定采用代码走查结合测试工L使用Q双齐下,争取比较d地解决系l的E_性问题?/p>
在代码走查的工作中,安排了对pȝ业务和开发语a工具比较熟悉的开发h员对应用的代码进行了交叉走查Q找Z码中存在的数据库q接声明和结果集未关闭、代码冗余和低效{故障若qԌ取得了良好的效果Q文中主要讲q结合工L使用对已l出现的内存泄漏问题的定位方法?/p>
内存泄漏的基本原?br />
在C++语言E序中,使用new操作W创建的对象Q在使用完毕后应该通过delete操作W显C地释放Q否则,q些对象占用堆I间Q永q没有办法得到回Ӟ从而引起内存空间的泄漏。如下的单代码就可以引v内存的泄?
void function(){
Int[] vec = new int[5];
}
在function()Ҏ执行完毕后,vec数组已经是不可达对象Q在C++语言中,q样的对象永q也得不到释放,U这U现象ؓ内存泄漏?/p>
而Java是通过垃圾攉?Garbage CollectionQGC)自动理内存的回ӞE序员不需要通过调用函数来释攑ֆ存,但它只能回收无用q且不再被其它对象引用的那些对象所占用的空间。在下面的代码中Q@环申请Object对象Qƈ所甌的对象放入一个Vector中,如果仅仅释放对象本nQ但是因为Vector仍然引用该对象,所以这个对象对GC来说是不可回收的。因此,如果对象加入到Vector后,q必MVector中删除,最单的Ҏ是Vector对象讄为null?/p>
Vector v = new Vector(10); for (int i = 1; i < 100; i++) { Object o = new Object(); v.add(o); o = null; }//此时Q所有的Object对象都没有被释放Q因为变量v引用q些对象?/td> |
实际上无用,而还被引用的对象QGC无能ؓ力了(事实上GC认ؓ它还有用)Q这一ҎD内存泄漏最重要的原因?/p>
Java的内存回收机制可以Ş象地理解为在堆空间中引入了重力场Q已l加载的cȝ静态变量和处于zdU程的堆栈空间的变量是这个空间的牵引对象。这里牵引对象是指按照Java语言规范Q即便没有其它对象保持对它的引用也不能够被回收的对象Q即Java内存I间中的本原对象。当然类可能被去加蝲Q活动线E的堆栈也是不断变化的,牵引对象的集合也是不断变化的。对于堆I间中的M一个对象,如果存在一条或者多条从某个或者某几个牵引对象到该对象的引用链Q则是可达对象Q可以Ş象地理解Z牵引对象伸出的引用链其拉住Q避免掉到回收池?而其它的不可辑֯象由于不存在牵引对象的拉力,在重力的作用下将掉入回收池。在?中,A、B、C、D、E、F六个对象都被牵引对象所直接或者间接地“牵引”,使得它们避免在重力的作用下掉入回收池。如果TR1-A铑֒TR2-D链断开Q则A、B、C三个对象׃失去牵引Q在重力的作用下掉入回收?被回?QD对象也是同样的原因掉入回收池Q而F对象仍然存在一个牵引链(TR3-E-F)Q所以不会被回收Q如??所C?br />
? 初始状?br />
? TR1-A铑֒TR2-D链断开QA、B、C、D掉入回收?br />
? A、B、C、D四个对象被回?/font>
通过前面的介l可以看刎ͼ׃采用了垃圑֛收机ӞM不可辑֯象都可以由垃圾收集线E回收。因此通常说的Java内存泄漏其实是指无意识的、非故意的对象引用,或者无意识的对象保持。无意识的对象引用是指代码的开发h员本来已l对对象使用完毕Q却因ؓ~码的错误而意外地保存了对该对象的引用(q个引用的存在ƈ不是~码人员的主观意?Q从而得该对象一直无法被垃圾回收器回收掉Q这U本来以为可以释放掉的却最l未能被释放的空间可以认为是被“泄漏了”?br />q里通过一个例子来演示Java的内存泄漏。假设有一个日志类LoggerQ其提供一个静态的log(String msg)ҎQQ何其它类都可以调用Logger.Log(message)来将message的内容记录到pȝ的日志文件中。LoggercL一个类型ؓHashMap的静态变量tempQ每ơ在执行log(message)Ҏ的时候,都首先将message的g入temp?以当前线E?当前旉为键)Q在Ҏ退Z前再从temp中将以当前线E和当前旉为键的条目删除。注意,q里当前旉是不断变化的Q所以logҎ在退Z前执行删除条目的操作q不能删除方法执行之初丢入的条目。这PM一个作为参CllogҎ的字W串最l由于被Logger的静态变量temp引用Q而无法得到回Ӟq种q背实现者主观意囄无意识的对象保持是我们所说的Java内存泄漏?/p>
鉴别泄漏对象的方?/strong>
一般说来,一个正常的pȝ在其q行E_后其内存的占用量是基本稳定的Q不应该是无限制的增长的Q同P对Q何一个类的对象的使用个数也有一个相对稳定的上限Q不应该是持l增长的。根据这L基本假设Q我们可以持l地观察pȝq行时用的内存的大和各实例的个数Q如果内存的大小持箋地增长,则说明系l存在内存泄漏,如果某个cȝ实例的个数持l地增长Q则说明q个cȝ实例可能存在泄漏情况?/p>
Optimizeit是Borland公司的品,主要用于协助对Y件系l进行代码优化和故障诊断Q其功能众多Q用方便,其中的OptimizeIt Profiler主要用于内存泄漏的分析。Profiler的堆视图(如图4)是用来观察pȝq行使用的内存大和各个cȝ实例分配的个数的Q其界面如图四所C,各列自左臛_分别为类名称、当前实例个数、自上个标记点开始增长的实例个数、占用的内存I间的大、自上次标记点开始增长的内存的大、被释放的实例的个数信息、自上次标记点开始增长的内存的大被释放的实例的个数信息Q表的最后一行是汇L据,分别表示目前JVM中的对象实例L、实例增长L、内存用L、内存用增长L{?/p>
在实践中Q可以分别在pȝq行四个时、八个小时、十二个时和二十四个小时时间点记录当时的内存状?x取当时的内存快照Q是工具提供的功能,q个快照也是供下一步分析?Q找出实例个数增长的前十位的c,记录下这十个cȝ名称和当前实例的个数。在记录完数据后Q点击Profiler中右上角的Mark按钮Q将该点的状态作Z一ơ记录数据时的比较点?/p>
? Profiler 堆视?br />
pȝq行二十四小时以后可以得到四个内存快照。对q四个内存快照进行综合分析,如果每一ơ快照的内存使用都比上一ơ有增长Q可以认定系l存在内存泄漏,扑և在四个快照中实例个数都保持增长的c,q些cd以初步被认定为存在泄漏?/p>
分析与定?/strong>
通过上面的数据收集和初步分析Q可以得出初步结?pȝ是否存在内存泄漏和哪些对象存在泄?被泄?Q如果结论是存在泄漏Q就可以q入分析和定位阶D了?/p>
前面已经谈到Java中的内存泄漏是无意识的对象保持Q简单地讲就是因为编码的错误D了一条本来不应该存在的引用链的存?从而导致了被引用的对象无法释放)Q因此内存泄漏分析的d是扑ևq条多余的引用链Qƈ扑ֈ其Ş成的原因。前面还讲到q牵引对象,包括已经加蝲的类的静态变量和处于zdU程的堆栈空间的变量。由于活动线E的堆栈I间是迅速变化的Q处于堆栈空间内的牵引对象集合是q速变化的Q而作为类的静态变量的牵引对象的集合在pȝq行期间是相对稳定的?/p>
Ҏ个被泄漏的实例对象,必然存在一条从某个牵引对象出发到达该对象的引用链。处于堆栈空间的牵引对象在被从栈中弹出后失d牵引的能力,变ؓ非牵引对象,因此Q在长时间的q行后,被泄露的对象基本上都是被作ؓcȝ静态变量的牵引对象牵引?/p>
Profiler的内存视N了堆视图以外Q还包括实例分配视图(?)和实例引用图(?)?/p>
Profiler的实例引用图为找Z牵引对象到泄漏对象的引用链提供了非常直接的方法,其界面的W二个栏目中昄的就是从泄漏对象出发的逆向引用链。需要注意的是,当一个类的实例存在泄漏时Qƈ非其所有的实例都是被泄漏的Q往往只有一部分是被泄漏对象Q其它则是正怋用的对象Q要判断哪些是正常的引用链,哪些是不正常的引用链(引v泄漏的引用链)。通过抽取多个实例q行引用囄分析l计以后Q可以找Z条或者多条从牵引对象出发的引用链Q下面的d是扑ևq条引用铑Ş成的原因?/p>
实例分配图提供的功能是对每个cȝ实例的分配位|进行统计,查看实例分配的统计结果对于分析引用链的Ş成具有一定的作用Q因为找到分配链与引用链的交点往往可以找C引用铑Ş成的原因Q下面将具体介绍?/p>
? 实例分配?br />
? 实例引用?br />
设想一个实例对象a在方法f中被分配Q最l被实例对象b所引用Q下面来分析从b到a的引用链可能的Ş成原因。方法f在创建对象a后,对它的用分为四U情?1、将a作ؓq回D?2、将a作ؓ参数调用其它Ҏ;3、在Ҏ内部a的引用传递给其它对象;4、其它情c其中情?不会造成由b到a的引用链的生成,不用考虑。下面考虑其它三种情况:对于1?两种情况Q其造成的结果都是在另一个方法内部获得了对象a的引用,它的分析与方法f的分析完全一?递归分析);考虑W?U情?1、假设方法f直接对象a的引用加入到对象bQ则对象b到a的引用链找CQ分析结?2、假设方法f对象a的引用加入到对象cQ则接下来就需要跟t对象c的用,对象c的分析比对象a的分析步骤更多一些,但大体原理都是一LQ就是跟t对象从创徏后被使用的历E,最l找到其被牵引对象引用的原因?/p>
现在泄漏对象的引用链以及引用链形成的原因找CQ内存泄漏测试与分析的工作就到此l束Q接下来的工作就是修改相应的设计或者实C的错误了?/p>
ȝ
使用上述的测试和分析ҎQ在实践中先后进行了三次试Q找Z好几处内存泄漏错误。系l的E_性得到很大程度的提高Q最初运?~2天就抛出内存溢出异常Q修改完成后Q系l从未出现过内存溢出异常。此Ҏ适用于Q何用Java语言开发的、对E_性有比较高要求的软gpȝ?/p>
(1)Easy:Java的语法比C++的相对简单,另一个方面就是Java能软g在很的机器上运行,基础解释其和cd的支持的大小Uؓ40kbQ增加基本的标准库和U程支持的内存需要增?25kb?/p>
(2)分布?Java带有很强大的TCP/IP协议族的例程库,Java应用E序能够通过URL来穿q网l来讉Kq程对象Q由于servlet机制的出玎ͼ使Java~程非常的高效,现在许多的大的web server都支持servlet?/p>
(3)OO:面向对象设计是把重点攑֜对象及对象的接口上的一个编E技?光向对象和C++有很多不同,在与多重l承的处理及Java的原cL型?/p>
(4)健壮Ҏ?Java采取了一个安全指针模型,能减重写内存和数据崩溃的可能型?/p>
(5)安全:Java用来设计|\和分布系l,q带来了新的安全问题QJava可以用来构徏防病毒和防攻ȝSystem.事实证明Java在防毒这一斚w做的比较好?/p>
(6)中立体系l构:Java~译其生成体pȝ构中立的目标文g格式可以在很多处理器上执行,~译器生的指o字节?Javabytecode)实现此特性,此字节码可以在Q何机器上解释执行?/p>
(7)可移植?Java中对基本数据l构cd的大和法都有严格的规定所以可UL性很好?/p>
(8)多线E?Java处理多线E的q程很简单,Java把多U程实现交给底下操作pȝ或线E程序完?所以多U程是Java作ؓ服务器端开发语a的流行原因之一?/p>
(9)Applet和servlet:能够在网上执行的程序叫AppletQ需要支持Java的浏览器很多Q而applet支持动态的|页Q这是很多其他语a所不能做到的?/p>
基本概念
1.OOP中唯一关系的是对象的接口是什么,像计算机的销售商她不电源内部结构是怎样的,他只关系能否l你提供电就行了Q也是只要知道can or not而不是how and why.所有的E序是由一定的属性和行ؓ对象l成的,不同的对象的讉K通过函数调用来完成,对象间所有的交流都是通过Ҏ调用Q通过对封装对象数据,很大限度上提高复用率?/p>
2.OOP中最重要的思想是类Q类是模板是蓝图Q从cM构造一个对象,卛_Zq个cȝ一个实?instance)?/p>
3.装:是把数据和行ؓl合起在一个包?q对对象使用者隐藏数据的实现q程Q一个对象中的数据叫他的实例字段(instance field)?/p>
4.通过扩展一个类来获得一个新cdl承(inheritance)Q而所有的c都是由Object根超cL展而得Q根类下文会做介绍?/p>
5.对象?个主要特?
behavior---说明q个对象能做什?
state---当对象施加方法时对象的反?
identity---与其他相D为对象的区分标志.
每个对象有唯一的indentity 而这3者之间相互媄?
6.cM间的关系:
use-a :依赖关系
has-a :聚合关系
is-a :l承关系--?Acȝ承了Bc,此时AcM仅有了BcȝҎQ还有其自己的方?(个性存在于共性中)
7.构造对象用构造器:构造器的提出,构造器是一U特D的ҎQ构造对象ƈ对其初始化?/p>
?Datacȝ构造器叫Data
new Data()---构造一个新对象Q且初始化当前时?
Data happyday=new Data()---把一个对象赋值给一个变量happydayQ从而该对象能够多ơ用,此处要声明的使变量与对象变量二者是不同?newq回的值是一个引用?/p>
构造器特点:构造器可以?个,一个或多个参数
构造器和类有相同的名字
一个类可以有多个构造器
构造器没有q回?br /> 构造器L和newq算W一起?
8.重蝲:当多个方法具有相同的名字而含有不同的参数Ӟ便发生重?~译器必L选出调用哪个Ҏ?/p>
9.?package)Java允许把一个或多个cL集在一hZl,UC包,以便于组lQ务,标准Java库分多包.java.lang java.util javaQnet{,包是分层ơ的所有的java包都在java和javax包层ơ内?/p>
10.l承思想:允许在已l存在的cȝ基础上构建新的类Q当你承一个已l存在的cLQ那么你复用了q个cȝҎ和字D,同时你可以在新类中添加新的方法和字段?/p>
11.扩展c?扩展cd分体Cis-a的承关p? 形式?class (子类) extends (基类)?/p>
12.多?在java中,对象变量是多态的.而java中不支持多重l承?/p>
13.动态绑?调用对象Ҏ的机制?/p>
(1)~译器检查对象声明的cd和方法名?/p>
(2)~译器检查方法调用的参数cd?/p>
(3)静态绑?若方法类型ؓpriavte static final ~译器会准确知道该调用哪个方法?/p>
(4)当程序运行ƈ且用动态绑定来调用一个方法时Q那么虚拟机必须调用x所指向的对象的实际cd相匹配的Ҏ版本?/p>
(5)动态绑?是很重要的特性,它能使程序变得可扩展而不需要重~译已存代码?/p>
14.finalc?为防止他Z你的cMz新类Q此cL不可扩展的?/p>
15.动态调用比静态调用花费的旉要长?/p>
16.抽象c?规定一个或多个抽象Ҏ的类本n必须定义为abstract?/p>
? public abstract string getDescripition
17.Java中的每一个类都是从ObjectcL展而来的?/p>
18.objectcM的equal和toStringҎ?/p>
equal用于试一个对象是否同另一个对象相{?/p>
toStringq回一个代表该对象的字W串Q几乎每一个类都会重蝲该方法,以便q回当前状态的正确表示.
(toString Ҏ是一个很重要的方?
19.通用~程:Mcȝ型的所有值都可以同objectcL的变量来代ѝ?/p>
20.数组列表:ArrayList动态数l列表,是一个类库,定义在java.uitl包中Q可自动调节数组的大?/p>
21.classc?objectcM的getclassҎq回ckasscd的一个实例,E序启动时包含在mainҎ的类会被加蝲Q虚拟机要加载他需要的所有类Q每一个加载的c都要加载它需要的cR?/p>
22.classcMؓ~写可动态操Ujava代码的程序提供了强大的功能反,q项功能为JavaBeans特别有用Q用反Java能支持VBE序员习惯用的工具?/p>
能够分析c能力的E序叫反器QJava中提供此功能的包叫Java.lang.reflect反射机制十分强大.
1.在运行时分析cȝ能力?br /> 2.在运行时探察cȝ对象?br /> 3.实现通用数组操纵代码?br /> 4.提供Ҏ对象?/p>
而此机制主要针对是工兯而不是应用及E序?/p>
反射机制中的最重要的部分是允许你检查类的结?用到的API?
java.lang.reflect.Field q回字段.
java.reflect.Method q回Ҏ.
java.lang.reflect.Constructor q回参数.
Ҏ指针:java没有Ҏ指针Q把一个方法的地址传给另一个方法,可以在后面调用它Q而接口是更好的解x案?/p>
23.接口(Interface)说明c该做什么而不指定如何dQ一个类可以实现一个或多个interface?/p>
24.接口不是一个类Q而是对符合接口要求的cȝ一套规范?/p>
若实C个接口需?个步?
1.声明c需要实现的指定接口?br /> 2.提供接口中的所有方法的定义?/p>
声明一个类实现一个接口需要用implements 关键?/p>
class actionB implements Comparable 其actionb需要提供CompareToҎQ接口不是类Q不能用new实例化一个接?
25.一个类只有一个超c,但一个类能实现多个接口。Java中的一个重要接口:Cloneable
26.接口和回?~程一个常用的模式是回调模式,在这U模式中你可以指定当一个特定时间发生时回调对象上的Ҏ?/p>
?ActionListener 接口监听.
cM的API?java.swing.JOptionPane
java.swing.Timer
java.awt.Tookit
27.对象clone:cloneҎ是object一个保护方法,q意味着你的代码不能单的调用它?/p>
28.内部c?一个内部类的定义是定义在另一个内部的cR?/p>
原因?
1.一个内部类的对象能够访问创建它的对象的实现Q包括私有数据?/p>
2.对于同一个包中的其他cL_内部c能够隐藏v来?/p>
3.匿名内部cd以很方便的定义回调?/p>
4.使用内部cd以非常方便的~写事g驱动E序?/p>
29.代理c?proxy):
1.指定接口要求所有代?/p>
2.objectcd义的所有的Ҏ(toString equals)
30.数据cd:Java是强调类型的语言Q每个变量都必须先申明它都类型,java中d?个基本类?4U是整型Q?U是点型,一U是字符型,被用于Unicode~码中的字符Q布型?br />