1.3 添加到Java 5中的语言特性 - 《Java高级编程(JDK6版)》

1.3  添加到Java 5中的语言特性 

Java 5中引入了几个有用的语法元素。所有这些特性都受更新编译器的支持,并且它们都转换为已定义的Java字节码,这意味着虚拟机不需要更新就可以执行这些特性:

  ●    泛型(Generics):一种使类实现类型安全的方式,这些类作用于任意对象类型,例如将一个集合的实例限定为持有一个特定对象类型并且当从集合获取一个对象时不需要进行强制对象转换。

  ●    增强的for循环(Enhanced for loop):提供了与迭代器一起使用的更简洁和更少出错的for循环版本。

  ●    可变参数(variable argument):支持将任意数量的参数传递给一个方法。

  ●    装箱/拆箱(Boxing/Unboxing):提供了在基本类型及其引用类型(例如int和Integer)之间自动转换的直接语言支持。

  ●    类型安全枚举(Type-safe enumerations):在语言级别上受支持的、定义并使用枚举的简洁语法。

  ●    静态导入(Static import):不需要将一个类的静态成员限定到类名称即可访问它们的能力。

  ●    元数据(Metadata):与第三方公司开发的新工具结合起来,可以通过自动生成代码来减少开发人员编写样本代码的工作。

Java语言推出的这些特性包括了开发人员在其他语言中使用的许多构造。它们使得Java编码更容易、更简洁,同时更快捷。即使您选择不利用这些特性,但是熟悉它们对于阅读和维护其他开发人员编写的代码也是非常重要的。

1.3.1  泛型

Java 5引入了泛型,也称为参数化类型。泛型允许编写可作用于任意类型的类,但是直到声明了类的实例,才指定特定的类型。因为此类型不是作为类定义的一部分而指定的,所以该类成为一般的、获得对任意指定的类型起作用的能力。最明显的大量使用泛型的例子是集合类。例如,ArrayList类被编写为只容纳Object。这意味着对象在添加到ArrayList时将失去其类型,并且在访问ArrayList的元素时需要进行强制转换。但是,使用ArrayList的一般版本的代码可以声明“我想要此ArrayList只容纳String”。这给Java增加了额外的类型安全性,因为如果String之外的类型添加到集合,那么编译器将会捕获它。这也意味着访问元素时不需要再进行强制转换,因为编译器知道它只容纳String,并在元素被视为String之外的类型时将会产生一个错误。将String指定为参数化类型就像将类型置于尖括号中一样容易:

ArrayList<String> listOfStrings;  // <TYPE_NAME> is new to the syntax

String stringObject;

listOfStrings = new ArrayList<String>(); // <TYPE NAME> is new to the syntax

listOfStrings.add(new String("Test string"));  // Can only pass in String objects

stringObject = listOfStrings.get(0);  // no cast required

泛型也称为参数化类型,其中的类型就是参数。正如上述示例中所见,String是正式类型参数。实例化参数化类型时必须使用与此相同的参数化类型。

因为Java 5中新语言特性的目的之一是不更改Java指令集,所以泛型基本上是句法糖(Syntatic Sugar)。访问ArrayList的元素时,编译器自动插入现在不必编写的强制转换。也有可能将基本数据类型用作参数化类型,但是要认识到这会招致装箱/拆箱的成本,因为它们隐式地与Object相互转换。虽然如此,仍然有增加类型安全性和程序可读性方面的优点。

1.类型消除

Java中的一般类型被编译为一个单独的类文件。每个正式的参数化类型没有一般类型的单独版本。泛型的实现利用类型消除,这意味着将实际参数化类型归纳为Object。奇怪地是,尽管不需要字节码发生更改,使用消除的决定阻碍其使用泛型机制来维护强类型化,正如即将见到的。

通过例子来说明,以下代码将不会编译:

interface Shape {

    void draw();

}

class Square implements Shape {

    public String name;

    public Square()

    {

        name  =  "Square";

    }

    public void draw()

    {

        System.out.println("Drawing square");

    }

}

public class ErasureExample {

    public static <T> void drawShape(T shape)

    {

        shape.draw();

    }

    public static void main(String args[])

    {

        Square square = new Square();

    }

}

编译器发布以下错误:

ErasureExample.java:23: cannot find symbol

symbol   : method draw()

location: class java.lang. Object

       shape.draw();

             ^

1 error

如果用以下方法替换drawShape方法,那么编译器现在很乐意编译该程序:

public static <T> void drawShape(T shape)

{

    System.out.println("Hashcode:  "+ shape.hashCode());

}

为何有此差异呢?这是由于类型消除所导致的结果。hashCode方法属于Object,而draw方法只属于Shape类型的对象。此小实验演示了参数化类型实际归纳为Object。下一个例了显示了涉及到使用具有不同参数化类型的一般类。

以一个新的一般类开始以容纳任意类型的数据项:

public class CustomHolder<E>

{

    E storedItem;

    public E getItem()

    {

        return(storedItem);

    }

    public void putItem(E item)

    {

        System.out.println("Adding data of type" + item.getClass().getName());

        storedItem = item;

    }

}

按照惯例,单个字母用于正式类型参数,通常E用于元素,T用于类型。向此类中添加一个main方法:

public static void main(String args[])

{

    CustomHolder<String> stringHolder = new CustomHolder<String>();

    CustomHolder<Object> objectHolder = new CustomHolder<Object>();

    String str = new String("test string");

    String str2;

    stringHolder.putItem(str);

    objectHolder.putItem(str);

    str2=stringHolder.getItem();

    //str2 = objectHolder.getItem();

}

考察最后两行。从stringHolder获取一个元素并将它分配给字符串没有问题。但是,如果取消注释第二行,这将试图访问objectHolder中相同的字符串,则会得到以下编译器错误。

c:\>javac CustomHolder.java

CustomHolder.java:28:  incompatible types

found    :  java.lang. Object

required:  java.lang.String

       str2 = objectHolder.getItem();

                                       ^

1 error

这有意义,因为实际类型参数(在本例中是String或Object)指示了类型。向objectHolder中添加一个String时,它只是作为Object存储。在试图将该Object分配给Sting时(在对objectHolder.getItem的调用中),您现在需要将它显式地强制转换为String类型。

由于类型消除,可以将一般的类引用分配给其非一般的(遗留)版本的引用。因此,以下代码编译时不会出错:

Vector oldVector;

Vector<Integer> intVector;

oldVector = intVector;  // valid

尽管不会出现错误,但是将非一般类的引用分配给一般类的引用将会导致未经检查的编译器警告。在消除更改方法的参数类型或原始类型的域分配(在消除更改方法/域类型的情况下)时,会发生这种情况。例如,以下程序将导致在它之后显示的警告。必须将命令行上的-Xlint:unchecked传递给javac,以查看具体的警告:

import java.util.*;

public class UncheckedExample {

    public void processIntVector(Vector<Integer> v)

    {

         // perform some processing on the vector

    }

    public static void main(String args[])

    {

         Vector<Integer> intVector = new Vector<Integer>();

         Vector oldVector = new Vector();

         UncheckedExample ue = new UncheckedExample();

         // This is permitted

         oldVector = intVector;

         // This causes an unchecked warning

         intVector = oldVector;

         // This is permitted

         ue.processIntVector(intVector);

         // This causes an unchecked warning

         ue.processIntVector(oldVector);

    }

}

试图编译上述代码将导致如下编译器警告:

UncheckedExample.java:16: warning: unchecked assignment:  java.util.Vector to

java.util.Vector<java.lang. Integer>

        intVector = oldVector;  // This causes an unchecked warning

UncheckedExampie.java:18: warning: unchecked method invocation:

processIntVector(java.util.Vector<java.lang.Integer>)  in UncheckedExample is

applied to (java.util.Vector)

          ue.processIntVector(oldVector);  // This causes an uncnecked warning

2 warnings

2.通配符和边界类型变量

因为不能将CustomHolder<Object>用作它就像是CustomHolder<String>的超类型一样,所以不能编写可以同时处理CustomHolder<Object>和CustomHolder<String>的方法。但有一种特殊的方法可以做到这一点。作为泛型语法的一部分引入了通配符,它在使用时基本上意指“任意类型的参数”。重新来看上一个例子并展示如何使用通配符(单个问号)。

获取CustomHolder类,并添加一些新方法和一个新的main,如下所示:

public static void processHolderObject(CustomHolder2<Object> holder)

{

    Object obj = holder.getItem();

    System.out.println("Item is:  "+ obj);

}

public static void processHolderString(CustomHolder2<String> holder)

{

    Object obj = holder.getItem();

    System.out.println("Item is:  " + obj);

}

public static void processHolderWildcard (CustomHolder2<?> holder)

{

    Object obj = holder.getItem();

    System.out.println("Item is: "+ obj);

}

public static void main(String args[])

{

    CustomHolder2<String> stringHolder = new CustomHolder2<String>();

    CustomHolder2<Object> objectHolder = new CustomHolder2<Object>();

    String str = new String("test string");

    String str2;

    stringHolder.putItem(str);

    objectHolder.putItem(str);

    //processHolderObject(stringHolder);

    processHolderObject(objectHolder);

    processHolderString(stringHolder);

    //processHolderString(objectHolder);

    processHolderWildcard(stringHolder);

    processHolderWildcard(objectHolder);

}

注释掉的两行将阻止程序的编译。如果这两行都取消注释,则编译器将发布以下错误:

c:\>javac CustomHolder2.java

CustomHolder2.java:48: processHolderObject(CustomHolder2<java.lang.Object>)  in

CustomHolder2<E> cannot be applied to (CustomHolder2<java.lang. String>)

         processHolderObject(stringHolder);

         ^

CustomHolder2.java:52: processHolderString(CustomHolder2<java.lang. String>)  in

CustomHolder2<E> cannot be applied to  (CustomHolder2<java.lang. Object>)

         processHolderString(objectHolder);

         ^

2 errors

这提醒您所使用的类型参数必须与正式类型参数匹配。但是请注意,调用processHolderWild- card的两行都没有注释掉。这是因为使用通配符允许您传入stringHolder或objectHolder。方法参数类型CustomerHolder2<?>可以读作“任意类型的CustomerHolder2”,而不是读作“Object类型的CustomerHolder2”或“String类型的CustomerHolder2”。

通过其调用边界,类型参数可以限制为某些类型。边界可应用于规则类型参数或通配符。再来看一下本章中前面的Shape例子,该例子定义了一个Shape接口:

import java.util.ArrayList;

import java.util. Iterator;

interface Shape {

    void draw();

}

class Square implements Shape {

    public void draw()

    {

        System.out.println("Drawing square");

    }

}

class Circle implements Shape {

    public void draw()

    {

        System.out.println("Drawing circle");

    }

}

现在定义一个PaintProgram类来演示边界。如果添加一个定义了类型参数的drawShape方法,则此方法不会工作:

public static <S> void drawShape(S shape)

{

    shape.draw();

}

所以必须向类型参数添加边界,这样Java才会将shape正式类型参数视为Shape而不是Object。通过将类型参数绑定到Shape,您可以指示传入的对象必须直接或间接派生自Shape。因此,Java知道该对象是Shape,并因此可以调用属于Shape的方法,而不只是调用Object方法:

public static <S extends Shape> void drawShapeBounded(S shape)

{

    shape.draw();

}

正如前面所暗示的,这使得您想知道泛型是否真是那么有用。如果必须显式陈述类型参数的边界,那么您也可以只使用Shape接口来约束正常方法参数。泛型真正发光的地方之一是简化了集合的使用,这可能是将泛型添加到Java所做的主要调整。

下面来实现drawAllShapes方法,它接收一个参数化的ArrayList。如预期的一样,此处需要一个边界,这样Java才不会将ArrayList的内容视为Object:

public static <T extends Shape> void drawAllShapes(ArrayList<T> shapeList)

{

    T shape;

    Iterator<T> shapeIterator;

    shapeIterator = shapeList.iterator();

    while(shapeIterator.hasNext())  {

        shape = shapeIterator.next();

        shape.draw();

    }

}

通过约束T类型参数,调用draw是可接受的,因为Java知道它是一个Shape。

如果想要指定多个用作边界的接口/类,那么请使用与符号(&)将它们分开。还应该注意,不管类型参数由接口还是类绑定,extends都用于指定边界。

3.使用泛型

创建一般类型的对象很简单。所有参数都必须匹配指定的边界。尽管您也许希望能够创建一个一般类型的数组,但是只有使用通配符类型参数时才有可能创建。也可以创建作用于一般类型的方法。本节将描述这些使用情况。

(1)类实例

创建一个一般类的对象包括为每个参数指定类型,以及为构造函数提供任意必需的参数。必须满足类型变量所有边界的条件。注意,在创建一个一般类的实例时,只有引用类型作为参数才是有效的。试图使用基本数据类型将导致编译器发布意外的类型错误。

以下是一个简单的HashMap的创建,它将Float分配给String:

HashMap<String,Float> hm = new HashMap<String,Float>();

(2)数组

一般类型的数组和类型变量的数组是不被允许的。例如,试图创建参数化Vectors的一个数组将会导致一个编译器错误:

import java.util.*;

public class GenericArrayExample {

     public static void main(String args[])

     {

          Vector<Integer> vectorList[] = new Vector<Integer>[10];

     }

}

如果试图编译该代码,那么编译器将会发布以下两个错误。此代码是创建一般类型数组的最简单方法,编译器会显式告知禁止创建一般类型的数组:

GenericArrayExample.java:6: arrays of generic types are not allowed

           Vector<Integer> vectorList[] = new Vector<Integer>[10];

                               ^

GenericArrayExample.java:6: arrays of generic types are not allowed

           Vector<Integer> vectorList[]  = new Vector<Integer>[10];

                                                      ^

2 errors

但是,您可通过将通配符用作类型参数来创建任意类型的数组。

(3)一般方法

除了类的一般机制外,还引入了一般方法。用于放置该参数的尖括号出现在其他所有方法修饰符之后,但在方法的返回类型之前。下面是一个一般方法声明的例子:

static <Elem> void swap(Elem[]  a,  int i , int j)

{

     Elem temp = a[i];

     a[i]  = a[j];

     a[j]  = temp;

}

用于一般方法参数的语法与用于一般类的语法相同。类型变量就像它们在类声明中一样可以有边界。两个方法不能有相同的名称和参数类型。如果两个方法有相同的名称和参数类型,并且具有相同数量的带有相同边界的类型变量,则这些方法都是相同的,并且编译器将会产生一个错误。

(4)泛型和异常

类型变量在catch子句中是不允许的,但是可用于方法的throws列表中。以下给出了一个在throws子句中使用类型变量的例子。Executor接口设计为执行一段也许会抛出一个异常的代码,该异常被指定为一个参数。在本例中,填充execute方法的代码可能抛出IOException。创建Executor接口的具体实例时,特定的IOException异常被作为一个参数指定:

import java.io.*;

interface Executor<E extends Exception>{

     void execute()  throws E;

}

public class GenericExceptionTest  {

     public static void main(String args[])   {

          try {

               Executor<IOException> e =

                    new Executor<IOException>()  {

                    public void execute()  throws IOException

                    {

                         // code here that may throw an

                         // IOException or a subtype of

                        //  IOException

                   }

              };

              e.execute();

         } catch(IOException ioe)  {

              System.out.println("IOException: "+ ioe);

              ioe.printStackTrace{};

         }

    }

}

在main中创建Executor类的一个实例时指定异常的特定类型。在创建一个具体的Executor接口实例之前,Execute方法将会抛出未知的任意异常。

1.3.2  增强的for循环

for循环已被修改为可以提供一种更清晰的方式来处理迭代器。在迭代器中使用for循环更容易出错,这是因为update子句置于循环体内,导致for循环的通常形式稍微有所破坏。一些语言使用foreach关键字来清理用于处理迭代器的语法。Java选择不引入一个新关键字,而是决定保持简化,同时引入了冒号的一个新用法。从传统角度来说,开发人员将会编写下列代码来使用迭代器:

for(Iterator iter = intArray.iterator(); iter.hasNext();  )  {

     Integer intObject = (Integer)iter.next();

     // ... more statements to use intObject ...

}

此代码的内在问题在于for循环中缺少update子句。使用迭代器的代码移到for循环体内是没有必要的,因为它也返回下一个对象。与以上代码段完成相同功能的新的、改进的语法如下:

for(Integer intObject  :  intArray)  {

     // ... same statements as above go here ...

}

此代码更简洁,更容易阅读。它消除了前面的构造代码向程序引入错误的所有潜在可能性。如果这段代码同一个一般集合一同使用,那么在编译时对象的类型将针对集合中的类型进行检查。

对新for循环提供支持只需要对编译器进行一处修改。产生的代码同按照传统方式编写的代码没有什么不同。例如,编译器也许会将上面的代码转换成如下所示:

for(Iterator<Integer> $iter = intArray.iterator() ; $iter.hasNext();  )  {

     Integer intObject = $iter.next();

     // ... statements ...

}

本例在标识符中使用美元符号仅仅意味着,在编译之前,编译器对新for循环语法的扩展生成一个更传统形式的惟一标识符。

在集合上使用迭代器与在数组上使用迭代器的语法相同。在数组上使用新的for循环与在集合上使用的语法是相同的:

for(String strObject : stringArray) {

    // ... statements here using strObject ...

}

然而,编译器扩充了数组版本的代码段,比集合版本稍微长一些:

String[] $strArray = stringArray;

for(int $i = 0;  $i < $strArray.length; $i++) {

     String strObject = $strArray[$i];

     // ... statements here ...

}

此时的编译器在扩充时使用了两个临时且惟一的变量。第一个变量是数组的一个别名,第二个变量是循环计数器。

1.3.3  Java类库的增强

为了完全支持新的for循环语法,被迭代的对象必须是一个数组或者直接或间接继承自一个新接口(java.lang.Iterable)。现有的集合类将被改进用于JDK 5版本。新Iterable接口如下所示:

public interface Iterable {

     /**

      * Returns an iterator over the elements in this collection.   There are no

      * guarantees concerning the order in which the elements are returned

      * (unless this collection is an instance of some class that provides a

      * guarantee).

      *

      * @return an Iterator over the elements in this collection.

      */

     SimpleIterator iterator();

}

此外,java.util.Iterator将会被改进以实现java.lang.ReadOnlyIterator,如下所示:

public interface ReadOnlyIterator {

     /**

      * Returns true if the iteration has more elements.  (In other

      * words,  returns true if next would return an element

      * rather than throwing an exception.)

      *

      * @return true if the iterator has more elements.

      */

     boolean hasNext();

     /**

      * Returns the next element in the iteration.

      *

      * @return the next element in the iteration.

      * @exception NoSuchElementException iteration has no more elements.

      */

     Object next();

}

此接口的引入消除了对java.util接口的依赖性。for循环语法的更改是语言级别上的,这样可以确保类库中需要的任意支持都被定位于java.lang分支。

1.3.4  可变参数

C和C++是支持函数可变长度参数列表的两种语言。Java决定引入这方面的优势。只在必要时才使用可变参数列表。如果滥用它们,就很容易创建出制造混乱的源代码。C语言在函数声明中使用省略号(…)来代表“任意数量的参数(0个或者多个)”。Java也使用省略号,但是将它同类型和标识符一起使用。这里的类型可以是任意内容,如任意类、任意基本类型,甚至是数组类型。然而,当在一个数组中使用它时,省略号必须出现在类型描述之前和方括号之后。由于可变参数的自然属性,每个方法只能有一个类型作为可变参数,同时它必须出现在参数列表的最后。

下面这个例子中的方法以任意数量的基本整数作为参数,并返回它们的总和:

public int sum(int... intList)

{

     int i, sum;

     sum=0;

     for(i=0; i<intList.length; i++)  {

          sum += intList[i];

     }

    return(sum);

}

从被标记为可变的参数位置算起,所有传入的参数都被组合成一个数组。这使得测试传入了多少参数变得简单。需要做的事情就是引用数组的length属性,同时数组还提供对每个参数的便捷访问。

以下是一个对任意数量元素的数组中的所有值求和的完整示例程序:

public class VarArgsExample {

     int sumArrays(int[]...  intArrays}

     {

          int sum,  i, j;

          sum=0;

          for(i=0;  i<intArrays.length;  i++)  {

               for(j=0;  j<intArrays[i].length;  j++)  {

                    sum += intArrays[i] [j];

               }

          }

          return(sum);

     }

     public static void main(String args[])

     {

          VarArgsExample va = new VarArgsExample();

          int sum=0;

          sum = va.sumArrays(new int[]{1,2,3},

                                 new int[]{4,5,6},

                                 new int[]{10,16});

          System.out.println("The sum of the numbers is:  " + sum);

     }

}

这段代码跟在已建立的方法之后用来定义和使用一个可变参数。省略号出现在方括号之后(也就是说,在可变参数的类型之后)。在该方法中,参数intArrays只是一个数组的数组。

1.3.5  装箱/拆箱转换

以前的Java语言存在着一种冗长乏味的处理方式,就是将基本类型(例如int和char)转换为它们的对应引用类型(例如,int对应的Integer,以及char对应的Character)时需要进行手工操作。摆脱这种经常性打包/拆包操作的解决方法就是使用装箱/拆箱转换。

1.装箱转换

装箱转换是一个隐式操作,它将基本类型(例如int)自动地放置到它对应的引用类型(在本例中是Integer)的一个实例中。拆箱则是相反的操作,即将一个引用类型(例如Integer)转换为它的基本类型(int)。如果没有装箱,就需要按如下方式将int基本类型添加到一个集合(它容纳Object类型)中:

Integer intObject;

int intPrimitive;

ArrayList arrayList = new ArrayList();

intPrimitive = 11;

intObject = new Integer(intPrimitive);

arrayList.put (intObject);  // cannot add intPrimitive directly

尽管此代码很简单,但是完全没必要这么冗长。随着装箱转换的引入,以上代码可以重新编写如下:

int intPrimitive;

ArrayList arrayList = new ArrayList();

intPrimitive = 11;

// here intPrimitive is automatically wrapped in an Integer

arrayList.put(intPrimitive);

这样,将不再需要创建一个Integer对象来将一个int放置到集合中。在结果引用类型的value()方法(例如Integer的intValue())等于源基本类型的值时,装箱转换将会发生。可以参考下面的表1-1来查看所有有效的装箱转换。如果存在任意其他的类型,那么装箱转换将是一种恒等转换(将类型转换为其自身类型)。注意,由于引入了装箱转换,一些引用基本类型的被禁转换也不再被禁止,因为基本类型现在也可以被转换成某些引用类型。

表1-1

基本类型

引用类型

基本类型

引用类型

boolean

Boolean

int

Integer

byte

Byte

long

Long

char

Character

float

Float

short

Short

double

Double

2.拆箱转换

Java也引入了拆箱转换,它将一个引用类型(例如Integer或者Float)转换成其自身的基本类型(例如int或者float)。可以参考下面的表1-2来查看所有有效的拆箱转换。在引用类型的value方法等于结果基本类型的值时,拆箱转换将会发生。

表1-2

引用类型

基本类型

引用类型

基本类型

Boolean

boolean

Integer

int

Byte

byte

Long

long

Character

char

Float

float

Short

short

Double

double

3.装箱/拆箱转换的有效上下文

因为装箱和拆箱操作是一种转换操作,所以它们无需程序员的特定指令即可自动发生(与类型转换(casting)不一样,类型转换是一种显式操作)。存在几种可以出现装箱/拆箱转换的上下文环境。

(1)赋值

当表达式的值被赋予一个变量时,会发生赋值转换。当表达式的类型与变量的类型不匹配,并且不会存在数据丢失的危险时,转换将会自动发生。发生转换的优先级首先是恒等转换,接着是扩展基本类型转换,然后是扩展引用类型转换,最后才是新的装箱(或者拆箱)转换。如果这些转换都无效,那么编译器将会发出一个错误。

(2)方法调用

进行方法调用时,如果参数类型没有精确匹配那些传入的参数,那么就可能发生一些转换。这些转换被认为是方法调用转换。每个在类型上没有精确匹配方法签名中对应参数的参数都将会被转换。可能发生的转换依次是恒等转换、扩展基本类型转换、扩展引用类型转换,然后是新的装箱(或者拆箱)转换。

在多个方法都匹配特定方法调用时,要选择最精确的方法。匹配最精确方法的规则外加装箱转换只需稍作修改。如果所有用于解决方法歧义的标准检查都失败了,那么装箱/拆箱转换将不会用于解决歧义。这样,在执行装箱转换检查时,方法调用将会被认为有歧义并且失败。

将装箱和泛型联合使用,可以编写如下代码:

import java.util.*;

public class BoxingGenericsExample {

     public static void main(String args[])

     {

          HashMap<String, Integer> hm = new HashMap<String,Integer>();

          hm.put("speed",  20);

     }

}

基本类型整数20自动转换成为一个Integer类型,然后按照指定关键字被放入到HashMap中。

1.3.6  静态导入

Java语言中引入了导入静态数据,以简化静态属性和方法的使用。在导入静态信息后,就可以使用方法/属性,而不需要限制方法/属性到所属类名称。例如,通过导入Math类的静态成员,就可以编写abs或者sqrt,而不用写成Math.abs和Math.sqrt。

这种机制同时还阻止了一种危险的编码实践,即将一组静态属性放入一个接口中,然后在每个需要使用这些属性的类中实现该接口。为了能够使用不受限制的属性,不应该实现下面的接口:

interface ShapeNumbers {

     public static int CIRCLE = 0;

     public static int SQUARE = 1;

     public static int TRIANGLE = 2;

}

实现这个接口会对ShapeNumbers接口产生不必要的依赖性。更糟糕的是,随着类的进化,特别是在其他类也需要访问这些常量,并且实现这个接口的情况下,对其进行维护会变得很困难。如果包含这些属性的接口进行了修改并且只有一些类被重新编译,那么已编译类相互之间很容易遇到同步问题。

为了更清楚地理解这点,将静态成员放入到一个类(而不是放入一个接口)中,然后通过一个已修改的导入指令语法导入。ShapeNumbers将修订如下:

package MyConstants;

class ShapeNumbers {

     public static int CIRCLE = 0;

     public static int SQUARE = 1;

     public static int TRIANGLE = 2;

}

然后,一个客户端类从ShapeNumbers类中导入静态信息,接着就能够使用CIRCLE、SQUARE和TRIANGLE属性,而不需要为它们加上ShapeNumbers和成员操作符前缀。

为了导入类中的静态成员,请在Java源程序文件的导入部分(顶部)指定如下代码:

import static MyConstants. ShapeNumbers.*;  // imports all static data

这行语法只是根据标准的导入语句格式进行了稍许修改。关键字static添加在import关键字之后,因为静态信息正在从一个特定的类中被导入,所以现在不是导入包,而总是添加类名称。关键字static添加到导入语句的主要原因是为了清晰地向那些读源代码的人显示静态信息的导入。

也可通过以下语法单独导入常量:

import static MyConstants.ShapeNumbers.CIRCLE;

import static MyConstants. ShapeNumbers. SQUARE;

这种语法也是所希望的。关键字static被包含进来,因为这是一个静态导入,并且要导入的静态信息片段被各自分开显式指定。

不能从默认包的一个类中静态地导入数据。类必须位于一个指定的包中。同时,静态属性和方法可能会产生冲突。例如,下面是包含静态常量的两个类(分别位于Colors.java和Fruits.java中):

package MyConstants;

public class Colors {

     public static int white = 0;

     public static int black = 1;

     public static int red = 2;

     public static int blue = 3;

     public static int green = 4;

     public static int orange = 5;

     public static int grey = 6;

}

package MyConstants;

public class Fruits {

     public static int apple = 500;

     public static int pear = 501;

     public static int orange = 502;

     public static int banana = 503;

     public static int strawberry = 504;

}

如果编写一个类,试图同时对这两个类进行静态导入,在使用一个同时在上述两个类中定义的静态变量之前,一切进展都很正常:

import static MyConstants. Colors.*;

import static MyConstants. Fruits.*;

public class StaticTest {

     public static void main(String args[])

     {

          System.out.println("orange = " + orange);

          System.out.println("color orange = " + Colors.orange);

          System.out.println("Fruity orange = " + Fruits.orange);

     }

}

上述程序的第七行将导致如下编译器错误。由于标识符orange在Colors和Fruits中都定义了,因此编译器无法解决这种分歧:

StaticTest.java:7:  reference to orange is ambiguous,  both variable orange in

MyConstants.Colors and variable orange in MyConstants. Fruits match

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值