Java类型系统
Java语言基础数据类型有两种:对象和基本类型(Primitives)。Java通过强制使用静态类型来确保类型安全,要求每个变量在使用之前必须先声明。
这种机制和非静态类型的语言有很大差别,非静态语言不要求对变量进行声明。虽然显式类型声明看起来较繁琐,但其有助于编译器对很多编程错误的预防,例如,由于变量名拼写错误导致创建了没有用的变量,调用了不存在的方法等。显式声明可以彻底防止这些错误被生成到运行代码中。关于Java类型系统的详细说明可以在Java语言规范(Java Language Specification)中找到。
基本类型
Java的基本类型不是对象,它们不支持对象相关的操作。基本数据类型只能通过一些预定义的操作符来修改它们。Java中的基本类型如下:
boolean(布尔型):值为true或false
byte(字节):8位二进制整数
short(短整型):16位二进制整数
int(整型):32位二进制整数
long(长整型):64位二进制整数
char(字符型):16位无符号整数,表示一个UTF-16编码单元
float(浮点型):32位IEEE-754标准的浮点数
double(双精度浮点型):64位IEEE-754标准的浮点数
对象和类
Java是一种面向对象的语言,其重点不是基础数据类型,而是对象(数据的组合及对这些数据的操作)。类(class)定义了成员变量(数据)和方法(程序),它们一起组成一个对象。在Java中,该定义(构建对象所用的模板)本身就是一种特定类型的对象,即类。在Java中,类是类型系统的基础,开发人员可以用它来描述任意复杂的对象,包括复杂的、专门的对象和行为。
与绝大多数面向对象的语言一样,在Java语言中,某些类型可以从其他类型继承而来。如果一个类是从另外一个类中继承来的,那么可以说这个类是其父类的子类(subtype或subclass),而其父类称为超类(supertype或superclass)。有多个子类的类可以称为这些子类的基类(base type)。
在一个类中,方法和成员变量的作用域都可以是全局的,在对象外可以通过对这个类的实例的引用来访问他们。
以下给出了一个非常简单的类的例子,它只有一个成员变量ctr和一个方法incr():
public class Trivial {
/* a field: its scope is the entire class */
private long ctr;
/* Modify the field */
public void incr() {
ctr++;
}
}
对象的创建
使用关键字new创建一个新的对象,即某个类的实例,如:
Trivial trivial = new Trivial();
在复制运算符"="的左边定义了一个变量,名为trivial。该变量的类型是Trivial,因此只能赋给它类型为Trivial的对象。赋值符右边为新创建的Trivial类的实例分配内存,并对该实例进行实体化。赋值操作符为新创建的对象变量分配引用。
在Trivial这个类中,变量ctr的定义是绝对安全的,虽然没有对它进行显式初始化。Java会保证给ctr的初始化值为0。Java会确保所有的字段在对象创建时自动进行初始化。布尔值初始化为false,基本数值类型初始化为0,所有的对象类型(包括String)初始化为null。上述的初始化赋值只适用于对象的成员变量,局部变量在被引用之前必须进行初始化。
可以在定义类时,通过构造函数更好地控制对象的初始化。构造函数的定义看起来很像一个方法,区别在于构造函数没有返回类型且名字必须和类名完全相同:
public class LessTrivial {
/* a field: its scope is the entire class */
private long ctr;
/* Constructor: initialize the fields */
public LessTrivial(long initCtr) {
ctr = initCtr;
}
/* Modify the field */
public void incr() {
ctr++;
}
}
事实上,Java中的每个类都会有一个构造函数。如果没有显式定义的构造函数,Java编译器会自动创建一个不带参数的构造函数。此外,如果子类的构造函数没有显式调用超类的构造函数,那么Java编译器会自动隐式调用超类的无参数的构造函数。前面给出了Trivial的定义(它没有显式地指定构造函数),实际上Java编译器会自动为它创建一个构造函数:
public Trivial() { super(); }
如上所示,由于LessTrivial类显式定义了一个构造函数,因此Java不会再给它隐式地定义一个默认的构造函数。这意味着如果创建一个没有参数的LessTrivial对象,会出现错误:
LessTrivial fail = new LessTrivial(); // Error!!
LessTrivial ok = new LessTrivial(18); // ...works
有两个不同的概念,需要对它们进行区分:“无参数的构造函数”和“默认的构造函数”。“默认的构造函数”是没有给一个类定义任何构造函数时,Java隐式地创建的构造函数,这个默认的构造函数刚好也是无参数的构造函数。而“无参数的构造函数”仅仅是没有参数的构造函数。Java不要求一个类包含没有参数的构造函数,也不需要定义无参数的构造函数,除非存在某些特定的需求。
如果一个类有多个构造函数,则最好采用级联(cascade)的方法创建它们,从而确保只会有一份代码对实例进行初始化,所有其他构造函数都调用它。为了便于说明,我们用一个例子来演示一下。为了更好地模拟常见情况,我们给LessTrivial类增加一个无参数的构造函数:
public class LessTrivial {
/* a field: its scope is the entire class */
private long ctr;
/* Constructor: init counter to 0 */
public LessTrivial() {
this(0);
}
/* Constructor: initialize the fields */
public LessTrivial(long initCtr) {
ctr = initCtr;
}
/* Modify the field */
public void incr() {
ctr++;
}
}
级联方法(cascading method)是Java中标准的用来为一些参数赋默认值的方法。一个对象的初始化代码应该统一放在一个单一、完整的方法或构造函数中,所有其他方法或构造函数只是简单地调用它。在级联方法中,在类的构造函数中必须显式调用其超类的构造函数。
构造函数应该是简单的,而且只应该包含为对象的成员变量指定一致性的初始状态的操作。举个列子,设计一个对象用来表示数据库或网络连接,可能会在构造函数中执行连接的创建、初始化和可用性的验证操作。虽然这看起来很合理,但实际上这种方法会导致代码模块化程度不够,从而难以调试和修改。更好的设计是构造函数只是简单地把连接状态初始化为closed,并另外创建一个方法来显式地设置网络连接。
对象类及其方法
Java类Object(java.lang.Object)是所有类的根类,每个Java对象都是一个Object。如果一个类在定义时没有显式指定其超类,它就是Object类的直接子类。Object类中定义了一组方法,这些方法是所有对象都需要的一些关键行为的默认实现。除非子类重写了(override)这些方法,否则都会直接继承自Object类。
Object类中的wait、notify和notifyAll方法是Java类并发支持的一部分。
toString方法是对象用来创建一个自我描述的字符串的方法。toString方法的一个有趣的使用方式是用于字符串连接,任何一个对象都可以和一个字符串进行连接。以下这个例子给出了输出相同消息的两种方法,它们的运行结果完全相同。在这两个方法中,都为Foo类创建了新的实例并调用其toString方法,随后把结果和文本字符串连接起来,随后输出结果:
System.out.println("This is a new foo: " + new Foo());
System.out.println("This is a new foo: ".concat((new Foo()).toString()));
在Object类中,toString方法的实现基于对象在堆中的位置,其返回一个没什么用的字符串。在代码中对toString方法进行重写是方便后期调试良好的开端。
clone方法和finalize方法属于历史遗留,只有在子类中重写finalize方法时,Java才会在运行时调用该方法。但是,当类显式地定义了finalize方法时,对该类的对象指向垃圾回收时会调用该方法。Java不但无法保证什么时候会调用finalize方法,实际上,它甚至无法确保一定会调用这个方法。此外,调用finalize方法可能会重新激活一个对象!其中的道理很复杂。当一个对象不存在可用的引用时,Java就会自动对它执行垃圾回收。但是finalize方法的实现会为这个对象“创建”一个新的可用的引用,例如把实现了finalize的对象加到某个列表中!由于这个原因,finalize方法的实现阻碍了对所定义的类的很多优化。使用finalize方法,不会带来什么好处,却带来了一堆坏处。
通过clone方法,可以不调用构造函数而直接创建对象。虽然在Object类中定义了clone方法,但在一个对象中调用clone方法会导致异常,除非该对象实现了Cloneable接口。当创建一个对象的代价很高时,clone方法可以成为一种有用的优化方式。虽然在某些特定情况下,使用clone方法可能是必须的,但是通过复制构造函数(以已有的实例作为其唯一参数)显得更简单,而且在很多情况下,其代价是可以忽略的。
Object类的最后两个方法是hashCode和equals,通过这两个方法,调用者可以知道一个对象是否和另一个对象相同。
在API文档中,Object类的equals方法的定义规定了equals的实现准则。equals方法的实现应确保具有以下4个特性,而且相关的声明必须始终为真:
自反性:x.equals(x)
对称性:x.equals(y) == y.equals(x)
传递性:(x.equals(y) && y.equals(z)) == x.equals(z)
一致性:如果x.equals(y)在程序生命周期的任意点都为真,只要x和y值不变,则x.equals(y)就始终为真
要满足这4大特性,实际上需要很细致工作,而且其困难程度可能超出预期。常见的错误之一是定义一个新的类(违反了自反性),它在某些情况下等价于已有的类。假设程序使用了已有的定义了类EnglishWeekdays的库,假设又定义了类FrenchWeekdays。显然,我们很可能会为FrenchWeekdays类定义equals方法,该方法和EnglishWeekdays相应的French等值进行比较并返回真。但是千万不要这么做!已有的EnglishWeekdays类看不到新定义的FrenchWeekdays类,因而它永远都无法确定你所定义的类的实例是否是等值的。因此,这种方式违反了自反性!
hashCode方法和equals方法应该是成对出现的,只要重写了其中一个方法,另外一个也应该重写。很多库程序把hashCode方法作为判断两个对象是否等价的一种优化方式。这些库首先比较两个对象的散列码,如果这两个对象的散列码不同,那么就没有必要执行代价更高的比较操作,因为这两个对象一定是不同的。散列码算法的特点在于计算非常快速,这方法可以很好地取代equals方法。一方面,访问大型数组的每个元素来计算其散列码,很可能还比不上执行真正的比较操作,而另一方面,通过散列码计算可以非常快速地返回0值,只是可能不是非常有用。
对象、继承和多态
Java支持多态(polymorphism),多态是面向对象编程的一个关键概念。对于某种语言,如果单一类型的对象具备不同的行为,则认为该语言具备多态性。如果某个类的子类可以被赋给其基础类型的变量,那么就认为这个类是多态的。
在Java中,声明子类的关键字是extends。Java继承的例子如下:
public class Car {
public void drive() {
System.out.println("Going down the road!");
}
}
public class Ragtop extends Car {
// override the parent's definition
public void drive() {
System.out.println("Top dowm!");
// optionally use a superclass method
super.drive();
System.out.println("Got the radio on!");
}
}
Ragtop是Car的子类。从前面的介绍中,可以知道Car是Object的子类。Ragtop重新定义(即重写)了Car的drive方法。Car和Ragtop都是Car类(但它们并不都是Ragtop类型),它们的drive方法有着不同的行为。
现在,我们来演示一个多态的例子:
Car auto = new Car();
auto.drive();
auto = new Ragtop();
auto.drive();
尽管吧Ragtop类型赋值给了Car类型的变量,但这段代码可以编译通过(虽然吧Ragtop类型赋值给Car类型的变量)。它还可以正确运行,并输出如下结果:
Going down the road!
Top dowm!
Going down the road!
Got the radio on!
auto这个变量在生命的不同时期,分别指向了两个不同的Car类型的对象引用。其中一个对象,不但是Car类型,也是其子类型Ragtop类型。auto.drive()语句的确切行为取决于该变量当前是指向基类对象的引用还是子类对象的引用,这就是所谓的多态行为。
类似很多其他的面向对象编程语言,Java支持类型转换,允许声明的变量类型为多态形式下的任意一种变量类型。
Ragtop funCar;
Car auto = new Car();
funCar = (Ragtop)auto; // ERROR! auto is a Car, not a Ragtop!
auto.drive();
auto = new Ragtop();
Ragtop funCar = (Ragtop) auto; // Works! auto is a Ragtop
auto.drive();
虽然类型转换(casting)在某种情况下是必要的,但过度使用类型转换会使得代码很杂乱。显然,根据多态规则,所有的变量都可以声明为Object类型,然后进行必要的转换,但是这种方式违背了静态类型(static typing)准则。
Java限制方法的参数(即真正参数)是多态的,表示形参所指向的对象类型。同样,方法的返回值也是多态的,返回声明的对象类型。举个例子,继续以之前的Car为例,以下代码片段可以正常编译和运行:
public class JoyRide {
private Car myCar;
public void park(Car auto) {
myCar = auto;
}
public Car whatsInTheGarage() {
return myCar;
}
public void letsGo() {
park(new Ragtop());
whatsInTheGarage().drive();
}
public static void main(String[] args) {
JoyRide joyRide = new JoyRide();
joyRide.letsGo();
}
}
在方法park的声明中,Car类型的对象是其唯一参数。但是在方法letsGo中,在调用它时传递的参数类型是Ragtop,即Car类型的子类。同样,变量myCar赋值的类型为Ragtop,方法whatsInTheGarage返回类型变量myCar的值。如果一个对象是Ragtop类型,当调用drive方法时,他会输出"Top down!"和"Got the radio on!"信息;另一方面,因为它又是Car类型,它还可以用于任何Car类型可用的方法调用中。这种子类型可取代父类型是多态的一个关键特征,也是其可以保证类型安全的重要因素。在编译阶段,一个对象是否和其用途兼容也已经非常清晰。类型安全使得编译器能够及早发现错误,这些错误如果只是在运行时才发现,那么发现这些错误的成本就会高很多。
Final声明和Static声明
Java有11个关键字可以用作声明的修饰符,这些修饰符会改变被声明对象的行为,有时是很重要的改变。例如,在前面的例子中使用了多次的关键字:public和private。这两个修饰符的作用是控制对象的作用域和可见性。在后面的章节中会更详细介绍它们。在本节中,我们将探讨的是另外两个修饰符,这两个修饰符是全面理解Java类型系统的基础:final和static。
如果一个对象的声明前面包含了final修饰符,则意味着这个对象的内容不能在被改变。类、方法、成员变量、参数和局部变量都可以是final类型。
当用final修饰类时,意味着任何为其定义子类的操作都会引发错误。举个例子,String类是final类型,因为作为其内容的字符串必须是不可改变的(也就是说,创建了一个字符串后,就不能够改变它)。如果你仔细考虑一下,就会发现,确保其内容不被改变的唯一方式就是确保不能以String类型为基类来创建子类。如果能够创建子类,例如DeadlyString,就可以吧DeadlyString类的实例作为参数,并在验证完其内容后,马上在代码中把该实例的值从"fred"改成"';DROP TABLE contacts;"(把恶意SQL注入到你的系统中,对你的数据库进行恶意修改)!
当用final修饰方法时,它表示子类不能重写(override)这个方法。开发人员使用final方法来设计继承性(inheritance),子类的行为必须和实现高度相关,而且不允许改变其实现。举个例子,一个实现了通用的缓存机制的框架可能会定义一个基类CacheableObject,编程人员使用该框架的子类型来创建每个新的可缓存的对象类型。然而,为了维护框架的完整性,CacheableObject可能需要计算一个缓存键(cache key),该缓存键对于各对象类型都是一致的。在这种情况下,该缓存框架就可以把其方法computeCacheKey声明为final类型。
当用final修饰变量——成员变量、参数和局部变量是,它表示一旦对该变量进行了赋值,就不能再改变。这个限制是由编译器负责保障的:不但变量的值"不会"发生改变,而且编译器必须能够证明它"不能"发生改变。用final修饰成员变量时,表示该成员变量的赋值必须在变量的声明或构造函数中指定。如果没有在变量的声明或构造函数中对final类型的成员变量进行初始化,或者试图在任何其他地方对它进行赋值,都会出现错误。
当用final修饰参数时,表示在这个方法内,该参数的值一直都是在调用时传递进来的那个值。如果对final类型的参数进行赋值,就会出现错误。当然,由于参数值很可能是某个对象的引用,对象内部的内容是有可能发生变化的。用关键字final修饰参数时,仅仅表示该参数不能被赋值。
注意:在Java中,参数都是按值传递:函数的参数就是调用时所传递值的一个副本。另外,在Java中,在大部分情况下,变量是对象的引用,Java只是复制引用,而不是整个对象!引用就是所传递的值!
final类型的变量只能对其赋值一次。由于使用一个没有初始化的变量在Java中会出现错误,因此final类型的变量只能够被赋值一次。该赋值操作可以在函数结束之前任何时候进行,当然要是在使用该参数之前。
静态(static)声明可以用于类,但不能用于类的实例。和static相对应的是dynamic(动态)。任何没有声明为static的实体,都默认的dynamic类型。任何没有声明为static的实体,都是默认的dynamic类型。下述例子是对这一特点的说明:
public class QuietStatic {
public static int classMember;
public int instanceMember;
}
public class StaticClient {
public static test() {
QuietStatic.classMember++;
QuietStatic.instanceMember++; // ERROR!!
QuietStatic ex = new QuietStatic();
ex.classMember++; // WARNING!!
ex.instanceMember++;
}
}
在这个例子中,QuietStatic是一个类,ex是该类的一个实例的引用。静态成员变量classMember是QuietStatic的成员变量,可以通过类名引用它(QuietStatic.classMember)。反之,instanceMember是QuietStatic类的实例的成员变量,通过类名引用它(QuietStatic.instanceMember)就会出现错误。这种处理机制是有道理的,因为可以存在很多个名字为instanceMember的不同的变量,每个变量属于QuietStatic类的一个实例。如果没有显式指定是哪个instanceMember,那么Java也不可能知道是哪个instanceMember。
正如下一组语句所示,Java确实允许通过实例引用来引用类的(静态)变量。这容易让人产生误解,被认为是不好的编程习惯。如果这么做,大多数编译器和IDE就会生成警告。
静态声明和动态声明的含义之间的区别很微妙。最容易理解的是静态成员变量和动态成员变量之间的区别。再次说明,静态定义在一个类中只有一份副本,而动态定义对于每个实例都有一份副本。静态成员变量保存的是一个类的所有成员所共有的信息。
public class LoudStatic {
private static int classMember;
private int instanceMember;
public void incr() {
classMember++;
instanceMember++;
}
@Override public String toString() {
return "classMember: " + classMember
+ ", instanceMember: " + instanceMember;
}
public static void main(String[] args) {
LoudStatic ex1 = new LoudStatic();
LoudStatic ex2 = new LoudStatic();
ex1.incr();
ex2.incr();
System.out.println(ex1);
System.out.println(ex2);
}
}
该程序的输出是:
classMember: 2, instanceMember: 1
classMember: 2, instanceMember: 1
在前面这个例子中,变量classMember的初始化值被设置为0。在两个不同的实例ex1和ex2中,分别调用incr()方法对它们执行递加操作,两个实例输出的classMember值都是2。变量instanceMember在每个实例中,其初始化也都是被设置为0。但是,每个实例只对自己的instanceMember执行递加操作,因此输出的instanceMember值都为1。
在上面两个实例中,静态类定义和静态方法定义的共同点在于静态对象在其命名空间内都是可见的,而动态对象只能通过每个实例的引用才可见。此外,相比之下,静态对象和动态对象的区别则更为微妙。
静态方法和动态方法之间的一个显著区别在于静态方法在子类中不能重写。举个例子,下面的代码在编译时会出错:
public class Star {
public static void twinkle() { }
}
public class Arcturus extends Star {
public void twinkle() {} // ERROE!!
}
public class Rigel {
// this one works
public void twinkle() {
Star.twinkle();
}
}
在Java中,几乎没有理由要使用静态方法。在Java的早期实现中,动态方法调用明显慢于静态方法。开发人员常常倾向于使用静态方法来“优化”其代码。在Android的即时编译Dalvik环境中,不再需要这种优化。过度使用静态方法通常意味着架构设计不良。
静态类和动态类之间的区别是最微妙的。应用中的绝大部分类都是静态的。类通常是在最高层声明和定义的——在任何代码块之外。默认情况下,所有的这些声明都是静态的;相反,很多其他声明,在某些类之外的代码块,默认情况下是动态的。虽然成员变量默认是动态的,其需要显示地使用静态修饰符才会是静态的,但类默认是静态的。
实际上,这完全符合一致性要求。根据对“静态”的定义(属于类但不属于类的实例),高层声明应该是静态的,因为他们不属于任何一个类。但是,如果是在代码块内定义的(例如在高层类内定义),那么类的定义默认也是动态的。因此,为了动态地声明一个类,只需要在另一个类内定义它(翻译不顺畅???)。
这一点也说明了静态类和动态类之间的区别。动态类能够访问代码块内的类(因为它属于实例)的实例成员变量,而静态类却无法访问。以下代码是对这个特点的示例说明:
public class Outer {
public int x;
public class InnerOne {
public int fn() { return x; }
}
public static class InnerTube {
public int fn() {
return x; // ERROR!!
}
}
}
public class OuterTest {
public void test() {
new Outer.InnerOne(); // ERROR!!!
new Outer.InnerTube();
}
}
稍加思考,这段代码就可理解。成员变量x是类Class的实例的成员变量,也就是说,可以有很多名字为x的变量,每个变量都是Outer的运行时实例的成员变量。类InnerTube是类Outer的一部分,但不属于任何一个Outer实例。因此,在InnerTube中午饭访问Outer的实例成员变量x。相反,由于类InnerOne是动态的,它属于类Outer的一个实例。因此可以把类InnerOne理解成隶属于类Outer的每个实例的独立的类(虽然不是这个含义,但实际上就是这么实现的)。因此,InnerOne能够访问其所属的Outer类的实例的成员变量x。
类OuterTest说明了对于成员变量,我们可以使用类名.内部静态类来定义,并可以使用该静态类型的类的内部定义Outer.InnerTube(在这个例子中,是创建该类的一个实例),而动态类型的类的定义只有在类的实例中才可用。
抽象类
在Java的声明中,如果将类及其一个或者多个方法声明为抽象类型,则允许这个类的定义中可以不包括这些方法的实现:
public abstract class TemplatedService {
public final void service() {
// subclasses prepare in their own ways
prepareService();
// ... but they all run the same service
runService();
}
public abstract void prepareService();
private final void runService() {
// implementation of the service...
}
}
public class ConcreteService extends TemplatedService {
void prepareService() {
// set up for the service
}
}
不能对抽象类进行实例化。抽象类的子类必须提供其父类的所有抽象方法的定义,或者该子类本身也定义成抽象类。
抽象类可以用于实现常见的模板模式,它提供可重用的代码块,支持在执行时自定义特定点。可重用代码块是作为抽象类实现的。子类通过实现抽象方法对模板自定义。
接口
其他编程语言(例如C++、Python和Perl)支持多继承,即一个对象可以有多个父类。多继承有时非常复杂,程序执行和预期的不同(如从不同的父类中继承两个相同名字的成员变量)。为了方便起见,Java不支持多继承。和C++、Python、Perl等不同,在Java中,一个类只能有一个父类。
和多继承性不同,Java支持一个类通过接口(interface)实现对多种类型的继承。
接口支持只对类型进行定义但不实现。可以把接口想象成一个抽象类,其所有的方法都是抽象方法。Java对一个类可以实现的接口的数量没有限制。
下面这个例子是关于Java接口和实现该接口的类的示例:
public interface Growable {
// declare the signatrue but not the implementation
void grow(Fertilizer food, Water water);
}
public interface Eatable {
// another signature with no implementation
void munch();
}
// An implementing class must implement all interface methods
public class Bean implements Growable, Eatable {
@Override
public void grow(Fertilizer food, Water water) {
// ...
}
@Override
public void munch() {
// ...
}
}
接口只是方法的声明,而没有方法的实现。
异常
Java Collections框架
Java Collections框架是Java最强大和便捷的工具之一,它提供了可以用来表示对象的集合(collections)的对象:list、set和map。Java Collections框架库的所有接口和实现都可以在java.util包中获取。
在java.util包中,几乎没有什么历史遗留类,基本都是Java Collections框架的一部分,最好记住这些类,并避免定义具有相同名字的类。这些类是Vector、Hashtable、Enumeration和Dictionary。
Collection接口类型
Java Collections库中的5种主要对象类型都是使用接口定义的,如下所示。
Collection:这是Collections库中所有对象的根类型。Collection表示一组对象,这些对象不一定是有序的,也不一定是可访问的,还可能包含重复对象。在Collection中,可以增加和删除对象,获取其大小并对它指向遍历(iterate)操作。
List:List是一种有序的集合。List中的对象和整数从0到length-1一一映射。在List中,可能存在重复元素。List支持Collection的所有操作。此外,在List中,可以通过get方法获取索引对应的对象,反之,也可以通过indexOf方法获取某个对象的索引。还可以用add(index,e)方法改变某个特定索引对应的元素。List的iterator(迭代器)按序依次返回各个元素。
Set:Set是一个无序集合,它不包含重复元素。Set也支持Collection的所有操作。但是,如果在Set中添加的是一个已经存在的元素,则Set的大小并不会改变。
Map:Map和List类似,其区别在于List把一组整数映射到一组对象中,而Map把一组key对象映射到一组value对象。与其他集合类一样,在Map中,可以增加和删除key-value对(键值对),获取其大小并对它执行遍历操作。Map的具体例子包括:把单词和单词定义的映射,日期和事件的映射,或URL和缓存内容的映射等。
Iterator:Iterator(迭代器)返回集合中的元素,其通过next方法,每次返回一个元素。Iterator是对集合黄总所有元素进行操作的一种较好的方式。
Collection实现方法
这些接口类型有多种实现方式,每个都有其适用的场景。最常见的实现方式包括以下几种。
ArrayList:ArrayList(数组列表)是一种支持数组特征的List。它在执行索引查找操作时很快,但是涉及改变其大小的操作的速度很慢。
LinkedList:LinkedList(链表)可以快速改变大小,但是查找速度很慢。
HashSet:HashSet是一个以hash方式实现的set。在HashSet中,增、删元素,判断是否包含某个元素及获取HashSet的大小这些操作都可以在常数级时间内完成。HashSet可以为空。
HashMap:HashMap是使用hash表作为索引,其实现了Map接口。在HashMap中,增、删元素,判断是否包含某个元素及获取HashMap的大小这些操作都可以在常数级时间内完成。它最多只可以包含一个空的key值,但是可以包含任意个value值为空的元素。
TreeMap:TreeMap是一个有序的Map。如果实现了Comparable接口,则TreeMap中的对象是按照自然序排序;如果没有实现Comparable接口,则是根据传递给TreeMap构造函数的Comparator类来排序。
经常使用Java的用户只要可能,往往倾向于使用接口类型的声明,而不是实现类型的声明。这是一个普遍的规则,但在Java Collections框架下最易于理解其中的原因。
Java泛型
垃圾收集
Java是一种支持垃圾收集的语言,这意味着代码不需要对内存进行管理。相反,我们的代码可以创建新的对象,可以分配内存,当不再需要这些对象时,只是停止使用这些对象而已。Dalvik运行时会自动删除这些对象,并适当地执行内存压缩。
在不远的过去,开发人员不得不为垃圾收集器担心,因为垃圾收集器可能会暂停下所有的应用处理以恢复内存,导致应用长时间、不可预测地、周期性地没有响应。很多开发人员,早起那些使用Java以及后来使用J2ME的开发人员,都还记得那些技巧、应对方式及不成文的规则来避免由早期垃圾收集器造成的长时间停顿和内存碎片。垃圾收集器机制在这些年有了很大改进。Dalvik明显不存在这些问题。创建新的对象基本上没有开销,只有那些对UI响应要求非常高的应用程序(例如游戏)需要考虑垃圾收集造成的程序暂停。
作用域
作用域决定了程序中的变量、方法和其他符号的可见范围。任何符号在其作用域外都是完全不可见的,不能被使用。
Java包
Java包提供了一种机制,它把相关类型分组到一个全局唯一的命名空间。这种分组机制可以防止在一个包的命名空间内的标识符合其他开发人员在其他命名空间内创建和使用的标识符冲突。
一个典型的Java程序有很多Java包的代码组成。典型的Java运行时环境提供了如java.lang和java.util.这样的包。此外,程序可能会依赖于其他通用的库,类似于org.apache树。传统上,应用代码(你所创建的代码)在你所创建的包内,包名是通过反转域名并附加程序名字生成的。因此,如果你的域名是androidhero.com,你的包所属的树的根是com.androidhero,则可以把代码放到如com.androidhero.awesomeprogram和com.androidhero.geohottness.service这样的包中。用于Android应用的典型的包在布局上会包含一个持久性包、UI包和复制应用逻辑和控制器代码的包。
包除了定义了全局唯一的命名空间之外,包内对象的成员(成员变量和方法)之间的可见性也不同。类的内部变量对于在同一个包内的类是可见的,而对于其他包内的类则是不可见的。
声明一个类属于某个包的方法是,在定义类的文件的最上方,使用package这个关键字按照下面这个方式声明:
package your.qualifieddomainname.fuctionalgrouping
不要过分简化包名!因为一个快速、临时的实现方式可能需要使用很多年,如果不能保证包名唯一,那么以后一定会深受其困扰。
一些大型的项目通过使用完全不同的顶级域名来实现公有API包和这些API的实现之间的隔离。举个例子,Android API使用顶级域名包android,这些API的实现则在com.android包内。Sun的Java源代码采用的机制也类似于此。公有API在Java包内,但是这些API的实现则放在了sun包内。在任意一种情况下,如果一个应用导入的是某个实现包,则这个应用会反复无常,因为它依赖与一些非公有的API。
虽然把代码添加到已有的包内是可以的,但通常认为这是一种不好的做法。通常情况下,除了命名空间,包通常是一颗源代码树,其至少和逆转的域名一样高。虽然这只是传统习惯,但是Java开发人员通常会期望com.brashandroid.coolapp.ui这个包中包含了CoolApp UI的所有源代码。如果另一颗树的某些地方也有CoolApp UI的一些代码,很多人会觉得不习惯。
访问修饰符和封装
类的成员有特殊的可见性规则。大多数Java块中的定义是有作用域的:它们只在代码块本身及内嵌于其中的代码块中可见。然而,类中的定义在代码块外也可能是可见的。Java支持类将其顶级成员(其方法和成员变量)通过访问修饰符(access modifiers)发布给其他类的代码。访问修饰符关键字修改了声明的可见性。
在Java中有3个访问修饰符关键字:public、protected和private。共支持4种访问级别。访问修饰符影响的是类的成员在类外面的访问性,但类内部的代码块遵循的是普通的作用域,不需要考虑访问修饰符的影响。
private修饰符的限制最高。带private关键字的声明在代码块外是不可见的。这种声明是最安全的,它会确保仅在类的内部还有指向这个声明的引用。private声明越多,类就越安全。
限制程度仅次于private修饰符的是默认的访问限制,即package访问。没有任何修饰符的声明属于默认情况,默认的访问可见性是指只能在同一个包中的其他类中可见。默认访问时创建对象共享的一种非常便捷的方式,Java的默认声明和C++中的friend声明类似。
protected访问修饰符除了支持所有的默认访问权限之外,还允许访问子类。任何包含protected声明的类都能够访问这些声明。
public访问修饰符是限制条件最弱的修饰符,其允许从任何地方对它进行访问。