Java 中文官方教程 2022 版(四十六)

原文:docs.oracle.com/javase/tutorial/reallybigindex.html

定义简单的通用类型

原文:docs.oracle.com/javase/tutorial/extra/generics/simple.html

这里是包java.util中接口ListIterator的定义的一个小节选:

public interface List <E> {
    void add(E x);
    Iterator<E> iterator();
}

public interface Iterator<E> {
    E next();
    boolean hasNext();
}

这段代码应该都很熟悉,除了尖括号中的内容。那些是接口ListIterator形式类型参数的声明。

类型参数可以在通用声明中的几乎任何地方使用,就像你会使用普通类型一样(尽管有一些重要的限制;请参阅细则部分)。

在介绍中,我们看到了List调用,比如List<Integer>。在调用(通常称为参数化类型)中,所有形式类型参数(在本例中为E)的所有出现都被实际类型参数(在本例中为Integer)替换。

你可能会想象List<Integer>代表了List的一个版本,其中E已经被Integer统一替换:

public interface IntegerList {
    void add(Integer x);
    Iterator<Integer> iterator();
}

这种直觉可能有所帮助,但也是误导的。

这很有帮助,因为参数化类型List<Integer>确实有看起来像这个展开的方法。

这是误导的,因为通用的声明实际上从未以这种方式展开。代码中没有多个副本——不在源代码中,也不在二进制代码中,也不在磁盘上,也不在内存中。如果您是 C++程序员,您会明白这与 C++模板非常不同。

通用类型声明只编译一次,并转换为单个类文件,就像普通类或接口声明一样。

类型参数类似于方法或构造函数中使用的普通参数。就像方法有描述其操作的值种类的形式值参数一样,通用声明有形式类型参数。当调用方法时,实际参数被替换为形式参数,并且方法体被评估。当调用通用声明时,实际类型参数被替换为形式类型参数。

关于命名约定的说明。我们建议您为形式类型参数使用简洁(如果可能的话是单个字符)但富有启发性的名称。最好避免在这些名称中使用小写字符,这样可以轻松区分形式类型参数和普通类和接口。许多容器类型使用E,表示元素,就像上面的例子一样。我们将在后面的例子中看到一些额外的约定。

泛型和子类型

原文:docs.oracle.com/javase/tutorial/extra/generics/subtype.html

让我们测试一下你对泛型的理解。以下代码片段是否合法?

List<String> ls = new ArrayList<String>(); // 1
List<Object> lo = ls; // 2 

第 1 行肯定是合法的。问题的棘手部分在于第 2 行。这归结为一个问题:StringList是否是ObjectList。大多数人本能地回答:“当然!”

好吧,看看接下来的几行:

lo.add(new Object()); // 3
String s = ls.get(0); // 4: Attempts to assign an Object to a String!

在这里,我们给lslo取了别名。通过别名lo访问String的列表ls,我们可以向其中插入任意对象。结果ls不再只包含String,当我们尝试从中取出东西时,会得到一个不愉快的惊喜。

Java 编译器当然会阻止这种情况发生。第 2 行将导致编译时错误。

一般来说,如果FooBar的子类型(子类或子接口),而G是某个泛型类型声明,那么G<Foo>不是G<Bar>的子类型。这可能是你需要了解的关于泛型最困难的事情,因为它违背了我们根深蒂固的直觉。

我们不应该假设集合不会改变。我们的直觉可能会让我们认为这些东西是不可变的。

例如,如果机动车管理部门向人口普查局提供驾驶员名单,这似乎是合理的。我们认为List<Driver>List<Person>,假设DriverPerson的子类型。实际上,传递的是驾驶员注册表的副本。否则,人口普查局可能会将不是驾驶员的新人加入列表,从而破坏了机动车管理部门的记录。

为了应对这种情况,考虑更灵活的泛型类型是很有用的。到目前为止,我们看到的规则相当严格。

通配符

原文:docs.oracle.com/javase/tutorial/extra/generics/wildcards.html

考虑编写一个打印集合中所有元素的例程的问题。以下是你可能在语言的旧版本(即 5.0 版本之前)中编写的方式:

void printCollection(Collection c) {
    Iterator i = c.iterator();
    for (k = 0; k < c.size(); k++) {
        System.out.println(i.next());
    }
}

这是一个使用泛型(和新的for循环语法)编写的天真尝试:

void printCollection(Collection<Object> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

问题在于这个新版本比旧版本要不实用得多。而旧代码可以使用任何类型的集合作为参数调用,新代码只接受Collection<Object>,正如我们刚刚证明的,这不是所有种类集合的超类型!

那么所有种类的集合的超类型是什么呢?它被写作Collection<?>(读作"未知类型的集合"),即元素类型匹配任何内容的集合。它被称为通配符类型,原因显而易见。我们可以这样写:

void printCollection(Collection<?> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

现在,我们可以使用任何类型的集合调用它。请注意,在printCollection()内部,我们仍然可以从c中读取元素并将它们赋予类型Object。这总是安全的,因为无论集合的实际类型是什么,它都包含对象。但是向其中添加任意对象是不安全的:

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // Compile time error

由于我们不知道c的元素类型代表什么,我们无法向其添加对象。add()方法接受类型为E的参数,即集合的元素类型。当实际类型参数为?时,它代表某种未知类型。我们传递给add的任何参数都必须是这种未知类型的子类型。由于我们不知道那种类型是什么,我们无法传递任何内容。唯一的例外是null,它是每种类型的成员。

另一方面,对于给定的List<?>,我们可以调用get()并利用结果。结果类型是一个未知类型,但我们始终知道它是一个对象。因此,将get()的结果分配给类型为Object的变量或将其作为期望类型为Object的参数传递是安全的。

有界通配符

考虑一个简单的绘图应用程序,可以绘制矩形和圆形等形状。为了在程序中表示这些形状,你可以定义一个类层次结构,如下所示:

public abstract class Shape {
    public abstract void draw(Canvas c);
}

public class Circle extends Shape {
    private int x, y, radius;
    public void draw(Canvas c) {
        ...
    }
}

public class Rectangle extends Shape {
    private int x, y, width, height;
    public void draw(Canvas c) {
        ...
    }
}

这些类可以在画布上绘制:

public class Canvas {
    public void draw(Shape s) {
        s.draw(this);
   }
}

任何绘图通常会包含许多形状。假设它们被表示为一个列表,那么在Canvas中有一个绘制它们所有的方法会很方便:

public void drawAll(List<Shape> shapes) {
    for (Shape s: shapes) {
        s.draw(this);
   }
}

现在,类型规则表明drawAll()只能在Shape的列表上调用:例如,不能在List<Circle>上调用。这很不幸,因为该方法所做的只是从列表中读取形状,所以它同样可以在List<Circle>上调用。我们真正想要的是该方法接受任何种类的形状列表:

public void drawAll(List<? extends Shape> shapes) {
    ...
}

这里有一个很小但非常重要的区别:我们用List<? **extends** Shape>替换了类型List<Shape>。现在drawAll()将接受任何Shape的子类列表,因此我们现在可以在List<Circle>上调用它。

List<? **extends** Shape>有界通配符的一个例子。?代表一个未知类型,就像我们之前看到的通配符一样。然而,在这种情况下,我们知道这个未知类型实际上是Shape的子类型。(注意:它可以是Shape本身,或者某个子类;它不一定要直接扩展Shape。)我们说Shape是通配符的上界

使用通配符灵活性的代价通常是很高的。这个代价是在方法体中写入shapes现在是非法的。例如,下面的写法是不允许的:

public void addRectangle(List<? extends Shape> shapes) {
    // *Compile-time error!*
    shapes.add(0, new Rectangle());
}

你应该能够弄清楚为什么上面的代码是不允许的。shapes.add()的第二个参数的类型是? **extends** Shape-- 一个未知的Shape子类型。由于我们不知道它的类型,我们也不知道它是否是Rectangle的超类型;它可能是也可能不是这样的超类型,因此在这里传递Rectangle是不安全的。

有界通配符正是处理 DMV 将其数据传递给人口普查局的例子所需的。我们的例子假设数据由从名称(表示为字符串)到人员(由Person或其子类型,如Driver表示的引用类型)的映射表示。Map<K,V>是一个接受两个类型参数的泛型类型的例子,表示映射的键和值。

再次注意正式类型参数的命名约定--K代表键,V代表值。

public class Census {
    public static void addRegistry(Map<String, ? extends Person> registry) {
}
...

Map<String, Driver> allDrivers = ... ;
Census.addRegistry(allDrivers);

通用方法

原文:docs.oracle.com/javase/tutorial/extra/generics/methods.html

考虑编写一个方法,该方法接受一个对象数组和一个集合,并将数组中的所有对象放入集合中。以下是第一次尝试:

static void fromArrayToCollection(Object[] a, Collection<?> c) {
    for (Object o : a) { 
        c.add(o); // *compile-time error*
    }
}

到目前为止,您已经学会了避免初学者的错误,即尝试将Collection<Object>用作集合参数的类型。您可能已经意识到,使用Collection<?>也行不通。请记住,您不能将对象随意放入未知类型的集合中。

处理这些问题的方法是使用通用方法。就像类型声明一样,方法声明也可以是通用的—即,由一个或多个类型参数参数化。

static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : a) {
        c.add(o); // *Correct*
    }
}

我们可以使用任何元素类型为数组元素类型的超类型的任何类型的集合调用此方法。

Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();

// *T inferred to be Object*
fromArrayToCollection(oa, co); 

String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();

// *T inferred to be String*
fromArrayToCollection(sa, cs);

// *T inferred to be Object*
fromArrayToCollection(sa, co);

Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();

// *T inferred to be Number*
fromArrayToCollection(ia, cn);

// *T inferred to be Number*
fromArrayToCollection(fa, cn);

// *T inferred to be Number*
fromArrayToCollection(na, cn);

// *T inferred to be Object*
fromArrayToCollection(na, co);

// *compile-time error*
fromArrayToCollection(na, cs);

请注意,我们不必向通用方法传递实际类型参数。编译器根据实际参数的类型为我们推断类型参数。它通常会推断使调用类型正确的最具体的类型参数。

一个问题是:何时应该使用通用方法,何时应该使用通配符类型?为了理解答案,让我们来看看Collection库中的一些方法。

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

我们可以在这里使用通用方法:

interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // *Hey, type variables can have bounds too!*
}

然而,在containsAlladdAll中,类型参数T仅使用一次。返回类型不依赖于类型参数,方法的任何其他参数也不依赖于类型参数(在这种情况下,只有一个参数)。这告诉我们,类型参数用于多态性;它的唯一效果是允许在不同的调用站点使用各种实际参数类型。如果是这种情况,应该使用通配符。通配符旨在支持灵活的子类型化,这正是我们要表达的。

通用方法允许使用类型参数来表示方法的一个或多个参数以及/或其返回类型之间的依赖关系。如果没有这样的依赖关系,则不应使用通用方法。

可以同时使用通用方法和通配符。以下是Collections.copy()方法的使用方法:

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

注意两个参数类型之间的依赖关系。从源列表src复制的任何对象必须可分配给目标列表dst的元素类型T。因此,src的元素类型可以是T的任何子类型—我们不关心是哪种类型。copy的签名使用类型参数表达依赖关系,但对第二个参数的元素类型使用通配符。

我们可以以另一种方式编写此方法的签名,而根本不使用通配符:

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

这是可以的,但是第一个类型参数既用于dst的类型,也用于第二个类型参数S的边界,而S本身只在src的类型中使用了一次—没有其他地方依赖于它。这表明我们可以用通配符替换S。使用通配符比声明显式类型参数更清晰、更简洁,因此在可能的情况下应优先使用通配符。

通配符还有一个优点,就是它们可以在方法签名之外使用,比如字段、局部变量和数组的类型。这里是一个例子。

回到我们的形状绘制问题,假设我们想要保留绘制请求的历史记录。我们可以在Shape类内部的静态变量中维护历史记录,并让drawAll()将其传入的参数存储到历史字段中。

static List<List<? extends Shape>> 
    history = new ArrayList<List<? extends Shape>>();

public void drawAll(List<? extends Shape> shapes) {
    history.addLast(shapes);
    for (Shape s: shapes) {
        s.draw(this);
    }
}

最后,让我们再次注意一下用于类型参数的命名约定。我们在没有更具体的类型来区分时使用T表示类型。这在泛型方法中经常发生。如果有多个类型参数,我们可能会使用字母来区分T在字母表中的邻居,比如S。如果泛型方法出现在泛型类中,最好避免在方法和类的类型参数中使用相同的名称,以避免混淆。嵌套泛型类也适用相同的规则。

与遗留代码互操作

原文:docs.oracle.com/javase/tutorial/extra/generics/legacy.html

到目前为止,我们所有的例子都假设了一个理想化的世界,在这个世界中,每个人都在使用支持泛型的最新版本的 Java 编程语言。

然而,现实情况并非如此。数百万行代码是用早期版本的语言编写的,它们不会一夜之间全部转换。

稍后,在将遗留代码转换为使用泛型部分,我们将解决将旧代码转换为使用泛型的问题。在本节中,我们将专注于一个更简单的问题:如何使遗留代码和通用代码互操作?这个问题有两个部分:在通用代码中使用遗留代码和在遗留代码中使用通用代码。

在通用代码中使用遗留代码

如何在使用自己的代码时使用旧代码,同时仍享受泛型的好处?

举个例子,假设你想使用包 com.Example.widgets。Example.com 的人们推出了一个用于库存控制的系统,其亮点如下所示:

package com.Example.widgets;

public interface Part {...}

public class Inventory {
    /**
     * Adds a new Assembly to the inventory database.
     * The assembly is given the name name, and 
     * consists of a set parts specified by parts. 
     * All elements of the collection parts
     * must support the Part interface.
     **/ 
    public static void addAssembly(String name, Collection parts) {...}
    public static Assembly getAssembly(String name) {...}
}

public interface Assembly {
    // *Returns a collection of Parts*
    Collection getParts();
}

现在,你想添加新代码,使用上面的 API。最好确保你总是使用正确的参数调用 addAssembly() - 也就是说,你传入的集合确实是 PartCollection。当然,泛型正是为此而设计的:

package com.mycompany.inventory;

import com.Example.widgets.*;

public class Blade implements Part {
    ...
}

public class Guillotine implements Part {
}

public class Main {
    public static void main(String[] args) {
        Collection<Part> c = new ArrayList<Part>();
        c.add(new Guillotine()) ;
        c.add(new Blade());
        Inventory.addAssembly("thingee", c);
        Collection<Part> k = Inventory.getAssembly("thingee").getParts();
    }
}

当我们调用 addAssembly 时,它期望第二个参数是 Collection 类型。实际参数是 Collection<Part> 类型。这样也能工作,但为什么呢?毕竟,大多数集合不包含 Part 对象,因此一般情况下,编译器无法知道 Collection 类型引用的是什么类型的集合。

在适当的通用代码中,Collection 总是伴随着一个类型参数。当像 Collection 这样的通用类型在没有类型参数的情况下使用时,它被称为原始类型

大多数人的第一反应是 Collection 真的意味着 Collection<Object>。然而,正如我们之前看到的,将 Collection<Part> 传递到需要 Collection<Object> 的地方是不安全的。更准确地说,类型 Collection 表示某种未知类型的集合,就像 Collection<?> 一样。

但等等,这也不对!考虑一下对 getParts() 的调用,它返回一个 Collection。然后将其赋给 k,它是一个 Collection<Part>。如果调用的结果是 Collection<?>,那么赋值就会出错。

实际上,这种赋值是合法的,但会生成一个未经检查的警告。这个警告是必要的,因为事实上编译器无法保证其正确性。我们无法检查 getAssembly() 中的遗留代码,以确保返回的确实是一个 Part 集合。代码中使用的类型是 Collection,可以合法地向这样的集合中插入各种对象。

那么,这不应该是一个错误吗?从理论上讲,是的;但从实际上讲,如果通用代码要调用遗留代码,这必须被允许。这取决于你,程序员,要确信在这种情况下,赋值是安全的,因为getAssembly()的契约规定它返回一个Part的集合,即使类型签名没有显示这一点。

因此,原始类型非常类似于通配符类型,但它们的类型检查不那么严格。这是一个故意的设计决定,允许泛型与现有的遗留代码进行交互。

从通用代码调用遗留代码本质上是危险的;一旦将通用代码与非通用遗留代码混合,通常提供的所有通用类型系统的安全保证都将无效。然而,你仍然比根本不使用泛型要好。至少你知道你这边的代码是一致的。

目前,非泛型代码比泛型代码要多得多,而且不可避免地会出现必须混合使用它们的情况。

如果你发现必须混合使用遗留代码和通用代码,请密切关注未经检查的警告。仔细考虑如何证明引发警告的代码的安全性。

如果你仍然犯了错误,导致警告的代码确实不安全,会发生什么?让我们看看这种情况。在这个过程中,我们将深入了解编译器的工作原理。

擦除和转换

public String loophole(Integer x) {
    List<String> ys = new LinkedList<String>();
    List xs = ys;
    xs.add(x); // *Compile-time unchecked warning*
    return ys.iterator().next();
}

在这里,我们给字符串列表和普通旧列表取了别名。我们将一个Integer插入列表,并尝试提取一个String。这显然是错误的。如果我们忽略警告并尝试执行此代码,它将在我们尝试使用错误类型的地方失败。在运行时,此代码的行为如下:

public String loophole(Integer x) {
    List ys = new LinkedList;
    List xs = ys;
    xs.add(x); 
    return(String) ys.iterator().next(); // *run time error*
}

当我们从列表中提取一个元素,并尝试将其强制转换为String以将其视为字符串时,我们将收到ClassCastException。与loophole()的泛型版本发生的事情完全相同。

这是因为,泛型是由 Java 编译器实现的一种称为擦除的前端转换。你(几乎)可以将其视为源到源的转换,其中loophole()的泛型版本转换为非泛型版本。

因此,即使存在未经检查的警告,Java 虚拟机的类型安全性和完整性也永远不会受到威胁

基本上,擦除会消除(或擦除)所有泛型类型信息。所有尖括号之间的类型信息都被丢弃,因此,例如,像List<String>这样的参数化类型被转换为List。所有类型变量的剩余用法都被替换为类型变量的上界(通常为Object)。并且,每当生成的代码不符合类型时,都会插入到适当类型的强制转换,就像loophole的最后一行一样。

擦除的全部细节超出了本教程的范围,但我们刚刚给出的简单描述并不离谱。了解一些关于这个是很有好处的,特别是如果您想要做一些更复杂的事情,比如将现有 API 转换为使用泛型(参见将旧代码转换为使用泛型部分),或者只是想了解为什么事情是这样的。

在旧代码中使用泛型代码

现在让我们考虑相反的情况。想象一下,Example.com 选择将他们的 API 转换为使用泛型,但是一些客户端还没有这样做。现在代码看起来像这样:

package com.Example.widgets;

public interface Part { 
    ...
}

public class Inventory {
    /**
     * Adds a new Assembly to the inventory database.
     * The assembly is given the name name, and 
     * consists of a set parts specified by parts. 
     * All elements of the collection parts
     * must support the Part interface.
     **/ 
    public static void addAssembly(String name, Collection<Part> parts) {...}
    public static Assembly getAssembly(String name) {...}
}

public interface Assembly {
    // *Returns a collection of Parts*
    Collection<Part> getParts();
}

客户端代码如下:

package com.mycompany.inventory;

import com.Example.widgets.*;

public class Blade implements Part {
...
}

public class Guillotine implements Part {
}

public class Main {
    public static void main(String[] args) {
        Collection c = new ArrayList();
        c.add(new Guillotine()) ;
        c.add(new Blade());

        // *1: unchecked warning*
        Inventory.addAssembly("thingee", c);

        Collection k = Inventory.getAssembly("thingee").getParts();
    }
}

客户端代码是在引入泛型之前编写的,但它使用了com.Example.widgets包和集合库,两者都使用了泛型类型。客户端代码中所有泛型类型声明的使用都是原始类型。

第 1 行生成了一个未经检查的警告,因为一个原始Collection被传递到一个期望Part集合的Collection位置,编译器无法确保原始Collection确实是Part集合。

作为一种替代方案,您可以使用源 1.4 标志编译客户端代码,确保不会生成任何警告。然而,在这种情况下,您将无法使用 JDK 5.0 引入的任何新语言特性。


细则

译文:docs.oracle.com/javase/tutorial/extra/generics/fineprint.html

一个泛型类被所有调用共享

以下代码片段打印什么?

List <String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());

你可能会倾向于说false,但你会错。它打印true,因为泛型类的所有实例具有相同的运行时类,而不考虑它们的实际类型参数。

实际上,使类成为泛型的是它对所有可能的类型参数具有相同行为的事实;同一个类可以被视为具有许多不同的类型。

因此,类的静态变量和方法也被所有实例共享。这就是为什么在静态方法或初始化程序中引用类型声明的类型参数,或在静态变量的声明或初始化程序中引用类型参数是非法的原因。

强制转换和 InstanceOf

泛型类被所有实例共享的事实的另一个含义是,通常没有意义询问一个实例是否是泛型类型的特定调用的实例:

Collection cs = new ArrayList<String>();
// *Illegal.*
if (cs instanceof Collection<String>) { ... }

同样,像这样的强制转换

// *Unchecked warning,*
Collection<String> cstr = (Collection<String>) cs;

给出一个未经检查的警告,因为这不是运行时系统会为你检查的内容。

类型变量也是如此

// *Unchecked warning.* 
<T> T badCast(T t, Object o) {
    return (T) o;
}

类型变量在运行时不存在。这意味着它们在时间和空间上都没有性能开销,这很好。不幸的是,这也意味着你不能可靠地在强制转换中使用它们。

数组

除非是(无界)通配符类型,否则数组对象的组件类型可能不是类型变量或参数化类型。你可以声明元素类型为类型变量或参数化类型的数组类型,但不能声明数组对象

这确实很烦人。这个限制是必要的,以避免出现这样的情况:

// *Not really allowed.*
List<String>[] lsa = new List<String>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// *Unsound, but passes run time store check*
oa[1] = li;

// *Run-time error: ClassCastException.*
String s = lsa[1].get(0);

如果允许参数化类型的数组,前面的例子将在没有任何未经检查警告的情况下编译,并在运行时失败。我们将类型安全作为泛型的主要设计目标。特别是,该语言被设计为保证如果整个应用程序使用javac -source 1.5编译时没有未经检查的警告,那么它是类型安全的

然而,你仍然可以使用通配符数组。前面代码的以下变体放弃了使用数组对象和元素类型为参数化的数组类型。因此,我们必须显式转换才能从数组中获取String

// *OK, array of unbounded wildcard type.*
List<?>[] lsa = new List<?>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// *Correct.*
oa[1] = li;
// *Run time error, but cast is explicit.*
String s = (String) lsa[1].get(0);

在下一个变体中,会导致编译时错误,我们避免创建元素类型为参数化的数组对象,但仍然使用具有参数化元素类型的数组类型。

// *Error.*
List<String>[] lsa = new List<?>[10];

同样,尝试创建元素类型为类型变量的数组对象会导致编译时错误:

<T> T[] makeArray(T t) {
    return new T[100]; // *Error.*
}

由于类型变量在运行时不存在,无法确定实际的数组类型。

解决这类限制的方法是使用类字面量作为运行时类型标记,如下一节所述,类字面量作为运行时类型标记。

类字面量作为运行时类型标记

原文:docs.oracle.com/javase/tutorial/extra/generics/literals.html

JDK 5.0 中的一个变化是类java.lang.Class是泛型的。这是一个有趣的例子,使用泛型性来做除了容器类之外的事情。

现在Class有一个类型参数T,你可能会问,T代表什么?它代表Class对象所代表的类型。

例如,String.class的类型是Class<String>Serializable.class的类型是Class<Serializable>。这可以用来提高反射代码的类型安全性。

特别是,由于Class中的newInstance()方法现在返回一个T,在通过反射创建对象时可以获得更精确的类型。

例如,假设你需要编写一个实用方法,执行数据库查询,给定一个 SQL 字符串,并返回与该查询匹配的数据库中的对象集合。

一种方法是显式传递一个工厂对象,在调用点编写代码如下:

interface Factory<T> { T make();} 

public <T> Collection<T> select(Factory<T> factory, String statement) { 
    Collection<T> result = new ArrayList<T>(); 

    /* *Run sql query using jdbc* */  
    for (/* *Iterate over jdbc results.* */) { 
        T item = factory.make();
        /* *Use reflection and set all of item's 
         * fields from sql results.* 
         */ 
        result.add(item); 
    } 
    return result; 
}

你可以这样调用

select(new Factory<EmpInfo>(){ 
    public EmpInfo make() {
        return new EmpInfo();
    }}, "selection string");

或者你可以声明一个类EmpInfoFactory来支持Factory接口

class EmpInfoFactory implements Factory<EmpInfo> {
    ...
    public EmpInfo make() { 
        return new EmpInfo();
    }
}

并调用它

select(getMyEmpInfoFactory(), "selection string");

这种解决方案的缺点是需要:

  • 使用冗长的匿名工厂类在调用点,或者

  • 为每种使用的类型声明一个工厂类,并在调用点传递一个工厂实例,这有点不自然。

将类字面量作为工厂对象是很自然的,然后可以通过反射来使用。今天(没有泛型的情况下)代码可能会这样写:

Collection emps = sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static Collection select(Class c, String sqlStatement) { 
    Collection result = new ArrayList();
    /* *Run sql query using jdbc.* */
    for (/* *Iterate over jdbc results.* */ ) { 
        Object item = c.newInstance(); 
        /* *Use reflection and set all of item's
         * fields from sql results.* 
         */  
        result.add(item); 
    } 
    return result; 
}

然而,这不会给我们提供我们想要的精确类型的集合。现在Class是泛型的,我们可以改为写如下代码:

Collection<EmpInfo> 
    emps = sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static <T> Collection<T> select(Class<T> c, String sqlStatement) { 
    Collection<T> result = new ArrayList<T>();
    /* *Run sql query using jdbc.* */
    for (/* *Iterate over jdbc results.* */ ) { 
        T item = c.newInstance(); 
        /* *Use reflection and set all of item's
         * fields from sql results.* 
         */  
        result.add(item);
    } 
    return result; 
} 

上面的代码以类型安全的方式给出了我们想要的精确类型的集合。

使用类字面量作为运行时类型标记的技术是一个非常有用的技巧。这是一个在新的用于操作注解的 API 中广泛使用的习语。

通配符更有趣

原文:docs.oracle.com/javase/tutorial/extra/generics/morefun.html

在本节中,我们将考虑通配符的一些更高级用法。我们已经看到了几个示例,在从数据结构中读取时有界通配符是有用的。现在考虑相反的情况,一个只写数据结构。接口Sink是这种类型的一个简单示例。

interface Sink<T> {
    flush(T t);
}

我们可以想象使用它,如下面的代码所示。方法writeAll()旨在将集合coll的所有元素刷新到接收器snk中,并返回最后一个刷新的元素。

public static <T> T writeAll(Collection<T> coll, Sink<T> snk) {
    T last;
    for (T t : coll) {
        last = t;
        snk.flush(last);
    }
    return last;
}
...
Sink<Object> s;
Collection<String> cs;
String str = writeAll(cs, s); // *Illegal call.*

如所写,对writeAll()的调用是非法的,因为无法推断出有效的类型参数;StringObject都不适合T的类型,因为Collection元素和Sink元素必须是相同类型。

我们可以通过修改writeAll()的签名来修复此错误,如下所示,使用通配符。

public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...}
...
// *Call is OK, but wrong return type.* 
String str = writeAll(cs, s);

现在调用是合法的,但赋值是错误的,因为推断的返回类型是Object,因为Ts的元素类型匹配,而sObject

解决方案是使用我们尚未看到的一种有界通配符形式:带有下界的通配符。语法? **super** T表示一个未知类型,它是T的超类型(或T本身;请记住超类型关系是自反的)。这是我们一直在使用的有界通配符的对偶,我们使用? **extends** T来表示一个未知类型,它是T的子类型。

public static <T> T writeAll(Collection<T> coll, Sink<? super T> snk) {
    ...
}
String str = writeAll(cs, s); // *Yes!* 

使用这种语法,调用是合法的,并且推断的类型是String,如所需。

现在让我们转向一个更现实的例子。java.util.TreeSet<E>表示一个按顺序排列的类型为E的元素树。构造TreeSet的一种方法是将Comparator对象传递给构造函数。该比较器将用于根据所需的顺序对TreeSet的元素进行排序。

TreeSet(Comparator<E> c) 

Comparator接口本质上是:

interface Comparator<T> {
    int compare(T fst, T snd);
}

假设我们想创建一个TreeSet<String>并传入一个合适的比较器,我们需要传递一个可以比较StringComparator。这可以通过Comparator<String>来完成,但Comparator<Object>同样有效。但是,我们将无法在Comparator<Object>上调用上面给出的构造函数。我们可以使用下界通配符来获得所需的灵活性:

TreeSet(Comparator<? super E> c) 

此代码允许使用任何适用的比较器。

作为使用下界通配符的最后一个示例,让我们看看方法Collections.max(),它返回作为参数传递给它的集合中的最大元素。现在,为了让max()起作用,传入的集合的所有元素都必须实现Comparable。此外,它们都必须可以相互比较。

对这种方法签名的泛型化的第一次尝试产生了:

public static <T extends Comparable<T>> T max(Collection<T> coll)

换句话说,该方法接受一个可与自身比较的某种类型T的集合,并返回该类型的一个元素。然而,这段代码实际上过于限制性。要了解原因,考虑一种可与任意对象进行比较的类型:

class Foo implements Comparable<Object> {
    ...
}
Collection<Foo> cf = ... ;
Collections.max(cf); // *Should work.*

cf的每个元素都可以与cf中的其他元素进行比较,因为每个这样的元素都是Foo,而Foo可以与任何对象进行比较,特别是与另一个Foo进行比较。然而,使用上面的签名,我们发现该调用被拒绝了。推断的类型必须是Foo,但Foo并没有实现Comparable<Foo>

T自身比较并不是必需的。所需的是T与其超类型之一进行比较。这给了我们:

public static <T extends Comparable<? super T>> 
        T max(Collection<T> coll)

请注意,Collections.max()的实际签名更为复杂。我们将在下一节将遗留代码转换为使用泛型中回到这一点。这种推理几乎适用于任何旨在适用于任意类型的Comparable的用法:您总是希望使用Comparable<? **super** T>

一般来说,如果您的 API 只使用类型参数T作为参数,那么它的使用应该利用下界通配符(? **super** T)。相反,如果 API 只返回T,那么使用上界通配符(? **extends** T)将为客户端提供更大的灵活性。

通配符捕获

现在应该很清楚了,鉴于:

Set<?> unknownSet = new HashSet<String>();
...
/* Add an element  t to a Set s. */ 
public static <T> void addToSet(Set<T> s, T t) {
    ...
}

下面的调用是非法的。

addToSet(unknownSet, "abc"); // *Illegal.*

实际传递的集合是字符串集合并不重要;重要的是作为参数传递的表达式是未知类型的集合,不能保证是字符串集合,或者是特定类型的集合。

现在,考虑以下代码:

class Collections {
    ...
    <T> public static Set<T> unmodifiableSet(Set<T> set) {
        ...
    }
}
...
Set<?> s = Collections.unmodifiableSet(unknownSet); // *This works! Why?*

看起来这不应该被允许;然而,看着这个具体的调用,允许它是完全安全的。毕竟,unmodifiableSet()对于任何类型的Set都有效,无论其元素类型是什么。

由于这种情况相对频繁出现,有一条特殊规则允许在非常具体的情况下使用这样的代码,其中可以证明代码是安全的。这条规则称为通配符捕获,允许编译器将通配符的未知类型推断为泛型方法的类型参数。

将遗留代码转换为使用泛型

原文:docs.oracle.com/javase/tutorial/extra/generics/convert.html

之前,我们展示了新代码和旧代码如何互操作。现在,是时候看看更难的问题,即"泛型化"旧代码。

如果决定将旧代码转换为使用泛型,需要仔细考虑如何修改 API。

您需要确保泛型 API 不会过于限制性;它必须继续支持 API 的原始契约。再次考虑一些java.util.Collection的示例。预泛型 API 看起来像:

interface Collection {
    public boolean containsAll(Collection c);
    public boolean addAll(Collection c);
}

一个天真的泛型尝试可能是以下内容:

interface Collection<E> {

    public boolean containsAll(Collection<E> c);
    public boolean addAll(Collection<E> c);
}

虽然这肯定是类型安全的,但它并不符合 API 的原始契约。containsAll()方法适用于任何类型的传入集合。只有当传入集合确实只包含E的实例时才会成功,但:

  • 传入集合的静态类型可能不同,也许是因为调用者不知道传入的集合的确切类型,或者是因为它是一个Collection<S>,其中SE的子类型。

  • 使用不同类型的集合调用containsAll()是完全合法的。该例程应该正常工作,返回false

addAll()的情况下,我们应该能够添加任何由E的子类型的实例组成的集合。我们看到如何在泛型方法部分正确处理这种情况。

您还需要确保修改后的 API 与旧客户端保持二进制兼容性。这意味着 API 的擦除必须与原始的未泛型化 API 相同。在大多数情况下,这是自然而然的,但也有一些微妙的情况。我们将检查我们遇到的最微妙的情况之一,即方法Collections.max()。正如我们在通配符的更多乐趣部分看到的,max()的一个合理签名是:

public static <T extends Comparable<? super T>> 
        T max(Collection<T> coll)

这很好,除了这个签名的擦除是:

public static Comparable max(Collection coll)

这与max()的原始签名不同:

public static Object max(Collection coll)

当然可以为max()指定这个签名,但没有这样做,所有调用Collections.max()的旧二进制类文件都依赖于返回Object的签名。

我们可以通过在形式类型参数T的边界中明确指定一个超类来强制擦除不同。

public static <T extends Object & Comparable<? super T>> 
        T max(Collection<T> coll)

这是给类型参数多重边界的一个示例,使用语法T1 & T2 ... & Tn。具有多重边界的类型变量被认为是边界中列出的所有类型的子类型。当使用多重边界时,边界中提到的第一个类型被用作类型变量的擦除。

最后,我们应该记住max只从其输入集合中读取,因此适用于T的任何子类型的集合。

这将我们带到 JDK 中实际使用的签名:

public static <T extends Object & Comparable<? super T>> 
        T max(Collection<? extends T> coll)

在实践中很少会出现这么复杂的情况,但是专家库设计者在转换现有 API 时应该准备好仔细思考。

另一个需要注意的问题是协变返回,即在子类中细化方法的返回类型。在旧的 API 中不应该利用这个特性。为了理解为什么,让我们看一个例子。

假设你原始的 API 形式为:

public class Foo {
    // *Factory. Should create an instance of* 
    // *whatever class it is declared in.*
    public Foo create() {
        ...
    }
}

public class Bar extends Foo {
    // *Actually creates a Bar.*
    public Foo create() {
        ...
    }
}

利用协变返回,你将其修改为:

public class Foo {
    // *Factory. Should create an instance of* 
    // *whatever class it is declared in.*
    public Foo create() {
        ...
    }
}

public class Bar extends Foo {
    // *Actually creates a Bar.*
    public Bar create() {
        ...
    }
}

现在,假设你的代码的第三方客户写了以下内容:

public class Baz extends Bar {
    // *Actually creates a Baz.*
    public Foo create() {
        ...
    }
}

Java 虚拟机不直接支持具有不同返回类型的方法重写。这个特性由编译器支持。因此,除非重新编译类Baz,否则它将无法正确重写Barcreate()方法。此外,Baz将需要修改,因为代码将被拒绝——Baz中的create()的返回类型不是Barcreate()的返回类型的子类型。

致谢

原文:docs.oracle.com/javase/tutorial/extra/generics/acknowledgements.html

Erik Ernst, Christian Plesner Hansen, Jeff Norton, Mads Torgersen, Peter von der Ahe 和 Philip Wadler 为本教程贡献了材料。

感谢 David Biesack, Bruce Chapman, David Flanagan, Neal Gafter, Orjan Petersson, Scott Seligman, Yoshiki Shibata 和 Kresten Krab Thorup 对本教程早期版本提供的宝贵反馈。如果有遗漏的人,请谅解。

教程:全屏独占模式 API

原文:docs.oracle.com/javase/tutorial/extra/fullscreen/index.html

作者:迈克尔·马塔克

您想在 Java 开发环境中使用高性能图形吗?您一直想编写游戏,但是您的图像移动得不够快吗?您的幻灯片程序因为无法控制用户的显示分辨率而无法正常工作吗?如果您一直在问这些问题,那么在 1.4 版本中引入的全屏独占模式 API 可能是您正在寻找的东西。

全屏独占模式

全屏独占模式是一个强大的新功能,它使您能够暂停窗口系统,以便直接在屏幕上进行绘制。

显示模式

本节描述了如何选择和设置显示模式。还讨论了为什么首先要设置显示模式。

被动渲染 vs. 主动渲染

本节讨论了被动渲染和主动渲染的优点。例如,在主事件循环中使用paint方法绘制是被动的,而在自己的线程中渲染是主动的。还列出了主动渲染的技巧。

双缓冲和页面翻转

本节解释了双缓冲并介绍了页面翻转,这是全屏独占模式中可用的双缓冲技术。

BufferStrategy 和 BufferCapabilities

本节介绍了java.awt.image.BufferStrategy类,该类允许您在不知道使用的缓冲区数量或显示它们的技术的情况下绘制到表面和组件。本节还回顾了java.awt.BufferCapabilities类,该类可以帮助您确定图形设备的功能。

示例

本页面列出了几个全屏独占模式示例。

全屏独占模式

原文:docs.oracle.com/javase/tutorial/extra/fullscreen/exclusivemode.html

使用微软的 DirectX API 的程序员可能已经熟悉全屏独占模式。其他程序员可能对这个概念有些陌生。无论哪种情况,全屏独占模式是 J2SE™ 1.4 版本的一个强大功能,允许程序员暂停窗口系统,以便直接向屏幕绘制。

这是与许多传统 GUI 程序有些微的范式转变。在传统的 Java GUI 程序中,AWT 负责从操作系统通过事件分发线程传播绘制事件,并在适当时调用 AWT 的Component.paint方法。在全屏独占应用程序中,绘制通常由程序自身主动完成。此外,传统的 GUI 应用程序受限于用户选择的屏幕位深和大小。在全屏独占应用程序中,程序可以控制屏幕的位深和大小(显示模式)。最后,许多更高级的技术,如翻页(下文讨论)和立体缓冲(利用为每只眼睛使用单独一组帧的系统)在某些平台上要求应用程序首先处于全屏独占模式。

硬件加速图像基础知识

要理解全屏独占模式 API,您需要了解一些关于硬件加速图像的基本原理。VolatileImage接口封装了一个表面,该表面可能会或可能不会利用硬件加速。这些表面可能会因操作系统的要求而失去其硬件加速或内存(因此称为“易失性”)。有关易失性图像的更多信息,请参阅VolatileImage 教程(即将推出)。

全屏独占模式通过java.awt.GraphicsDevice对象处理。要获取所有可用屏幕图形设备的列表(在单个或多监视器系统中),可以在本地java.awt.GraphicsEnvironment上调用getScreenDevices方法;对于默认(主要)屏幕(在单监视器系统中唯一的屏幕),可以调用getDefaultScreenDevice方法。

一旦获得了图形设备,可以调用以下方法之一:

  • public boolean isFullScreenSupported()

    此方法返回全屏独占模式是否可用。在不支持全屏独占模式的系统上,最好在固定大小的窗口模式下运行应用程序,而不是设置全屏窗口。

  • public void setFullScreenWindow(Window w)

    给定一个窗口,该方法使用该窗口进入全屏独占模式。如果全屏独占模式不可用,则窗口将定位在(0,0)并调整大小以适应屏幕。使用带有null参数的方法退出全屏独占模式。

编程提示

以下是关于使用全屏独占模式进行编程的一些建议:

  • 在进入全屏独占模式之前检查isFullScreenSupported。如果不支持,性能可能会降低。

  • 进入和退出全屏模式时,使用try...finally子句更加健壮。这不仅是良好的编码实践,还可以防止程序停留在全屏独占模式中的时间超过应有的时间:

    GraphicsDevice myDevice;
    Window myWindow;
    
    try {
        myDevice.setFullScreenWindow(myWindow);
        ...
    } finally {
        myDevice.setFullScreenWindow(null);
    }
    
    
  • 大多数全屏独占应用程序更适合使用无装饰窗口。使用setUndecorated方法在框架或对话框中关闭装饰。

  • 全屏独占应用程序不应该是可调整大小的,因为调整全屏应用程序的大小可能会导致不可预测的(或可能危险的)行为。

  • 出于安全原因,在应用程序中使用全屏独占模式时,用户必须授予fullScreenExclusive权限。

显示模式

原文:docs.oracle.com/javase/tutorial/extra/fullscreen/displaymode.html

一旦应用程序处于全屏独占模式,它可能能够利用主动设置显示模式。显示模式(java.awt.DisplayMode)由尺寸(以像素为单位的监视器宽度和高度)、位深度(每像素的位数)和刷新率(监视器更新自身的频率)组成。一些操作系统允许您同时使用多个位深度,此时特殊值BIT_DEPTH_MULTI用于位深度的值。此外,一些操作系统可能无法控制刷新率(或者您可能不关心刷新率设置)。在这种情况下,特殊值REFRESH_RATE_UNKNOWN用于刷新率值。

如何设置显示模式

要获取当前显示模式,只需在图形设备上调用getDisplayMode方法。要获取所有可能的显示模式列表,请调用getDisplayModes方法。无论您是否处于全屏独占模式,都可以随时调用getDisplayModegetDisplayModes

在尝试更改显示模式之前,您应该首先调用isDisplayChangeSupported方法。如果此方法返回false,则操作系统不支持更改显示模式。

只有在全屏独占模式下才能更改显示模式。要更改显示模式,请使用所需的显示模式调用setDisplayMode方法。如果显示模式不可用,不支持显示模式更改,或者您未在全屏独占模式下运行,则会抛出运行时异常。

更改显示模式的原因

设置显示模式的主要原因是性能。如果应用程序选择显示的图像与屏幕具有相同的位深度,应用程序可以运行得更快。此外,如果您可以依赖显示器具有特定尺寸,那么绘制到该显示器会简单得多,因为您不必根据用户设置的显示方式缩放或放大物体。

编程提示

以下是选择和设置显示模式的一些建议:

  • 在尝试更改图形设备上的显示模式之前,请检查isDisplayChangeSupported方法返回的值。

  • 在尝试更改显示模式之前,请确保您处于全屏独占模式。

  • 与使用全屏模式一样,设置显示模式在try...finally子句中更加健壮:

    GraphicsDevice myDevice;
    Window myWindow;
    DisplayMode newDisplayMode;
    DisplayMode oldDisplayMode 
        = myDevice.getDisplayMode();
    
    try {
        myDevice.setFullScreenWindow(myWindow);
        myDevice.setDisplayMode(newDisplayMode);
        ...
    } finally {
        myDevice.setDisplayMode(oldDisplayMode);
        myDevice.setFullScreenWindow(null);
    }
    
    
  • 在为应用程序选择显示模式时,您可能希望保留首选显示模式列表,然后从可用显示模式列表中选择最佳显示模式。

  • 作为备用方案,如果您想要的显示模式不可用,您可能希望以固定大小的窗口模式运行。

被动渲染 vs. 主动渲染

原文:docs.oracle.com/javase/tutorial/extra/fullscreen/rendering.html

正如之前提到的,大多数全屏应用程序通常在绘制时更好地处于控制状态。在传统的窗口化 GUI 应用程序中,绘制的时间通常由操作系统处理。在窗口化环境中,这是完全合理的。窗口化应用程序不知道用户何时会移动、调整大小、暴露或被另一个窗口覆盖,直到实际发生。在 Java GUI 应用程序中,操作系统向 AWT 传递一个绘画事件,AWT 找出需要绘制的内容,创建一个带有适当裁剪区域的java.awt.Graphics对象,然后使用该Graphics对象调用paint方法:

// Traditional GUI Application paint method:
// This can be called at any time, usually 
// from the event dispatch thread
public void paint(Graphics g) {
    // Use g to draw my Component
}

这有时被称为被动渲染。可以想象,这样的系统会产生很多开销,让许多性能敏感的 AWT 和 Swing 程序员感到恼火。

在全屏独占模式下,您不再需要担心窗口被调整大小、移动、暴露或遮挡(除非您忽略了我关于关闭调整大小的建议)。相反,应用程序窗口直接绘制到屏幕上(主动渲染)。这样做可以简化绘制过程,因为您永远不需要担心绘画事件。事实上,在全屏独占模式下,由操作系统传递的绘画事件甚至可能在不适当或不可预测的时间传递。

在全屏独占模式下,不要依赖paint方法,绘制代码通常更适合在渲染循环中完成:

public void myRenderingLoop() {
    while (!done) {
        Graphics myGraphics = getPaintGraphics();
        // Draw as appropriate using myGraphics
        myGraphics.dispose();
    }
}

这样的渲染循环可以在任何线程中完成,可以是自己的辅助线程,也可以作为主应用程序线程的一部分。

编程提示

一些关于使用主动渲染的提示:

  • 不要将绘制代码放在paint方法中。您永远不知道该方法何时会被调用!相反,使用另一个方法名,比如render(Graphics g),在窗口模式下可以从paint方法中调用,或者在渲染循环中使用自己的图形调用。

  • 使用setIgnoreRepaint方法在应用程序窗口和组件上关闭所有从操作系统完全分派的绘画事件,因为这些事件可能在不适当的时间调用,或者更糟糕的是,调用paint,这可能导致 AWT 事件线程和您的渲染循环之间的竞争条件。

  • 将绘制代码与渲染循环分开,以便在全屏独占和窗口模式下都能完全运行。

  • 优化您的渲染,以便您不会始终在屏幕上绘制所有内容(除非您使用翻页或双缓冲,下面将讨论)。

  • 不要依赖updaterepaint方法来传递绘画事件。

  • 不要使用重量级组件,因为这些组件仍会产生涉及 AWT 和平台窗口系统的开销。

  • 如果您使用轻量级组件,比如 Swing 组件,您可能需要稍微调整它们,以便它们使用您的Graphics进行绘制,而不是直接调用paint方法。请随意从您的渲染循环直接调用 Swing 方法,如paintComponentspaintComponentpaintBorderpaintChildren

  • 如果您只想要一个简单的全屏 Swing 或 AWT 应用程序,可以随意使用被动渲染,但请记住,在全屏独占模式下,绘制事件可能有些不可靠或不必要。此外,如果您使用被动渲染,您将无法使用更高级的技术,如翻页。最后,请务必小心避免死锁,如果您决定同时使用主动和被动渲染—这种方法并不推荐。

双缓冲和页面翻转

原文:docs.oracle.com/javase/tutorial/extra/fullscreen/doublebuf.html

假设您必须逐像素或逐行在屏幕上绘制整个图片。如果您直接将这样的东西绘制到屏幕上(使用Graphics.drawLine),您可能会发现需要一些时间。您甚至可能会注意到您的图片是如何绘制的可见痕迹。与观看以这种方式和速度绘制事物不同,大多数程序员使用一种称为双缓冲的技术。

在 Java 应用程序中,双缓冲的传统概念相当简单:创建一个屏幕外图像,使用图像的图形对象绘制到该图像,然后,在一步中,使用目标窗口的图形对象和屏幕外图像调用drawImage。您可能已经注意到,Swing 在其许多组件中使用了这种技术,通常默认启用,使用setDoubleBuffered方法。

屏幕表面通常被称为主表面,用于双缓冲的屏幕外图像通常被称为后备缓冲区。将内容从一个表面复制到另一个表面的行为通常被称为块线传输,或blitting(blt 通常发音为"blit",不应与 BLT 三明治混淆)。

双缓冲

主表面通常通过任何显示组件的图形对象进行操作;在全屏模式下,使用全屏窗口的图形进行任何操作是对屏幕内存的直接操作。因此,您可以利用全屏独占模式中可能由于窗口系统的开销而无法使用的其他功能。在全屏独占模式中仅可用的一种技术是一种称为页面翻转的双缓冲形式。

页面翻转

许多显卡都有视频指针的概念,它只是视频内存中的一个地址。这个指针告诉显卡在下一个刷新周期期间要显示的视频内容在哪里。在一些显卡和一些操作系统中,这个指针甚至可以通过编程方式进行操作。假设你在视频内存中创建了一个与屏幕精确宽度、高度和位深度相同的后备缓冲区,然后像使用双缓冲区一样向该缓冲区绘制。现在想象一下,如果不像双缓冲区那样将图像传输到屏幕上,而是简单地将视频指针更改为你的后备缓冲区,那会发生什么。在下一次刷新期间,显卡现在将使用你的图像来显示。这种切换称为翻页,与基于 blt 的双缓冲区相比,性能提升在于只需要在内存中移动一个指针,而不是将整个内容从一个缓冲区复制到另一个缓冲区。

当发生页面翻转时,旧后备缓冲区的指针现在指向主表面,旧主表面的指针现在指向后备缓冲区内存。这会自动为下一个绘制操作做好准备。

翻页

有时在翻页链中设置多个后备缓冲区是有利的。当绘制所花费的时间大于显示器的刷新率时,这是特别有用的。翻页链简单地是两个或更多后备缓冲区(有时称为中间缓冲区)加上主表面(有时称为三重缓冲、四重缓冲等)。在翻页链中,下一个可用的后备缓冲区变为主表面,依此类推,一直到用于绘制的最后一个后备缓冲区。

双缓冲和翻页的好处

如果你的性能指标只是双缓冲或翻页发生的速度与直接渲染相比,你可能会感到失望。你可能会发现,直接渲染的数字远远超过双缓冲的数字,而双缓冲的数字远远超过翻页的数字。这些技术都是用于改善感知性能,在图形应用程序中比数值性能更重要。

双缓冲主要用于消除可见的绘制,这可以使应用程序看起来业余、迟缓或闪烁。翻页主要用于消除撕裂,这是一种在绘制到屏幕上比显示器的刷新率更快时发生的分裂效果。更平滑的绘制意味着更好的感知性能和更好的用户体验。

BufferStrategy 和 BufferCapabilities

原文:docs.oracle.com/javase/tutorial/extra/fullscreen/bufferstrategy.html

BufferStrategy

在 Java 2 标准版中,您无需担心视频指针或视频内存就可以充分利用双缓冲或翻页。新的类java.awt.image.BufferStrategy已经添加,以便处理绘制到表面和组件的通用方式,无论使用的缓冲区数量或显示它们的技术。

缓冲策略为您提供了两种通用的绘图方法:getDrawGraphicsshow。当您想开始绘图时,获取一个绘图对象并使用它。当您完成绘图并希望将信息呈现到屏幕上时,请调用 show。这两种方法被设计得相当优雅,可以很好地适应渲染循环:

BufferStrategy myStrategy;

while (!done) {
    Graphics g = myStrategy.getDrawGraphics();
    render(g);
    g.dispose();
    myStrategy.show();
}

缓冲策略还设置了帮助您监视VolatileImage问题。在全屏独占模式下,VolatileImage问题尤为重要,因为窗口系统有时会收回它给您的视频内存。一个重要的例子是当用户在 Windows 中按下ALT+TAB组合键时,突然您的全屏程序在后台运行,您的视频内存就会丢失。您可以调用contentsLost方法来查看是否发生了这种情况。同样,当窗口系统将内存还给您时,您可以使用contentsRestored方法来查找。

BufferCapabilities

如前所述,不同的操作系统,甚至同一操作系统上的不同显卡,都有不同的可用技术。这些能力被暴露出来,以便您可以为应用程序选择最佳技术。

java.awt.BufferCapabilities封装了这些功能。每个缓冲策略都由其缓冲能力控制,因此为您的应用程序选择正确的缓冲能力非常关键。要找出哪些功能可用,请从您的图形设备上可用的GraphicsConfiguration对象调用getBufferCapabilities方法。

Java 2 标准版 1.4 版中提供的功能有:

  • isPageFlipping

    此功能返回此图形配置上是否可用硬件翻页。

  • isFullScreenRequired

    此功能返回在尝试硬件翻页之前是否需要全屏独占模式。

  • isMultiBufferAvailable

    此功能返回硬件中是否可用多缓冲(两个或更多后备缓冲区加上主表面)。

  • getFlipContents

    此功能返回用于执行硬件翻页的技术提示。这很重要,因为在使用的技术不同的情况下,show后的后备缓冲区内容也会有所不同。返回的值可以为 null(如果isPageFlipping返回false)或以下值之一。只要isPageFlipping方法返回 true,就可以为缓冲策略指定任何值,尽管性能会根据可用功能而变化。

  • FlipContents.COPIED

    此值意味着后备缓冲区的内容已复制到主表面。"翻页"可能是作为硬件位块传输执行的,这意味着硬件双缓冲可能是通过位块传输而不是真正的翻页完成的。这在理论上应该更快,或者至少与从VolatileImage到主表面的位块传输一样快,尽管实际效果可能有所不同。翻页后,后备缓冲区的内容与主表面相同。

  • FlipContents.BACKGROUND

    此值意味着后备缓冲区的内容已用背景颜色清除。可能已执行真正的翻页或位块传输。

  • FlipContents.PRIOR

    此值意味着后备缓冲区的内容现在是旧主表面的内容,反之亦然。通常,此值表示真正的翻页发生,尽管这并不是保证,再次强调,此操作的效果可能有所不同。

  • FlipContents.UNKNOWN

    此值意味着翻页后的后备缓冲区内容未定义。您可能需要尝试找出哪种技术最适合您(或者您可能不在乎),并且您肯定需要每次绘制时自己设置后备缓冲区的内容。

要为组件创建缓冲策略,请调用createBufferStrategy方法,提供所需的缓冲区数量(此数字包括主表面)。如果需要任何特定的缓冲技术,请提供适当的BufferCapabilities对象。请注意,当您使用此方法的版本时,必须在您的选择不可用时捕获AWTException。还要注意,这些方法仅适用于CanvasWindow

一旦为组件创建了特定的缓冲策略,您可以使用getBufferStrategy方法对其进行操作。请注意,此方法也仅适用于画布和窗口。

编程提示

关于使用缓冲能力和缓冲策略的一些建议:

  • try...finally子句中更加稳健地获取、使用和释放图形对象:

    BufferStrategy myStrategy;
    
    while (!done) {
        Graphics g;
        try {
            g = myStrategy.getDrawGraphics();
            render(g);
        } finally {
            g.dispose();
        }
        myStrategy.show();
    }
    
    
  • 在使用缓冲策略之前,请检查可用的功能。

  • 为了获得最佳效果,请在全屏独占窗口上创建您的缓冲策略。在使用翻页之前,请确保检查isFullScreenRequiredisPageFlipping功能。

  • 不要对性能做任何假设。根据需要调整你的代码,但要记住不同的操作系统和显卡具有不同的功能。对你的应用程序进行性能分析!

  • 你可能想要对你的组件进行子类化,以重写createBufferStrategy方法。使用一个适合你的应用程序的策略选择算法。FlipBufferStrategyBltBufferStrategy内部类是受保护的,可以被子类化。

  • 不要忘记你可能会丢失你的绘图表面!在绘制之前一定要检查contentsLostcontentsRestored。所有已丢失的缓冲区在恢复时都必须重新绘制。

  • 如果在 Swing 应用程序中使用缓冲策略进行双缓冲,你可能希望关闭 Swing 组件的双缓冲,因为它们已经是双缓冲的。视频内存有一定的价值,应仅在绝对必要时使用。

  • 使用多个后备缓冲区可能会导致浪费。当绘制时间超过执行show的时间时,多缓冲仅有用。对你的应用程序进行性能分析!

示例

原文:docs.oracle.com/javase/tutorial/extra/fullscreen/example.html

CapabilitiesTest 展示了在运行该程序的机器上可用的不同缓冲能力。

DisplayModeTest 展示了一个使用被动渲染的 Swing 应用程序。如果全屏独占模式可用,它将进入全屏独占模式。如果允许显示模式更改,它允许您在显示模式之间切换。

MultiBufferTest 进入全屏模式,并通过活动渲染循环使用多缓冲。

课程:准备 Java 程序员语言认证

原文:docs.oracle.com/javase/tutorial/extra/certification/index.html

Oracle 为 Java SE 8 程序员提供了两条认证路径:

想要学习 Java 语言的高级指南,请参阅 Java 教程学习路径。

教程:Java 命名和目录接口

原文:docs.oracle.com/javase/tutorial/jndi/index.html

本教程描述了 JNDI™(Java 命名和目录接口)是一个用于访问目录和命名服务的 API。在这里,您将了解基本的命名和目录服务以及如何使用 JNDI 编写简单的应用程序来使用这些服务。最流行的目录服务 LDAP 被用来演示使用 JNDI 访问目录服务。

从这里开始,了解命名和目录概念的概述。

为您提供了 JNDI、其架构和打包的概述。

描述了设置运行本教程中描述的示例以及任何其他 JNDI 应用程序所需的环境的说明和步骤。

描述了各种命名和目录操作,并通过许多示例演示了使用 JNDI 访问命名/目录服务。

针对 LDAP 用户的专门课程。它讨论了将 JNDI 建模为 LDAP API、如何执行 LDAP 认证、SSL 和在生产环境中管理连接等内容。

展示了如何将您的应用程序与目录集成,以便您可以将 Java 对象存储到目录中并从目录中检索。

介绍了 JDK 5.0 和 JDK 6 中可用的 JNDI 和 LDAP 服务提供程序中的功能。


注意: 本教程基于位于 docs.oracle.com/javase/jndi/tutorial/ 的独立 JNDI 教程。最后更新于 Java 2 SDK,标准版,v 1.4.2,全面介绍了 JNDI,但不再受支持。本教程从独立教程中摘录了基础知识,并包括了在 Java 平台标准版 5.0 和 6 发行版中添加的功能。

旧版 JNDI 教程被保留为 docs.oracle.com 上的存档。

教训:命名和目录概念

原文:docs.oracle.com/javase/tutorial/jndi/concepts/index.html

命名概念

在任何计算系统中的一个基本设施是命名服务——通过名称与对象关联并根据其名称找到对象的方法。在几乎任何计算机程序或系统中使用时,您总是在命名一个对象或另一个对象。例如,当您使用电子邮件系统时,必须提供收件人的名称。要访问计算机中的文件,必须提供其名称。命名服务允许您根据名称查找对象。

名称用于引用对象。

命名服务的主要功能是将人们友好的名称映射到对象,例如地址、标识符或通常由计算机程序使用的对象。

例如,Internet 域名系统(DNS)将机器名称映射到 IP 地址:

www.example.com ==> 192.0.2.5

文件系统将文件名映射到程序可以使用的文件引用,以访问文件的内容。

c:\bin\autoexec.bat ==> File Reference

这两个示例也说明了命名服务存在的广泛规模范围——从在互联网上命名对象到在本地文件系统上命名文件。

名称

要在命名系统中查找对象,您需要提供对象的名称。命名系统确定名称必须遵循的语法。这种语法有时被称为命名系统的命名约定。名称由组件组成。名称的表示包括一个组件分隔符,标记名称的组件。

命名系统 组件分隔符 名称
UNIX 文件系统 "/" /usr/hello
DNS "." sales.Wiz.COM
LDAP "," 和 "=" cn=Rosanna Lee, o=Sun, c=US

UNIX 文件系统的命名约定是,文件的命名来自于相对于文件系统根目录的路径,路径中的每个组件从左到右使用斜杠字符("/")分隔。例如,UNIX 的路径名 /usr/hello,命名了位于文件系统根目录中的文件目录usr中的文件hello

DNS 命名约定要求 DNS 名称中的组件从右到左排序,并用点字符(".")分隔。因此,DNS 名称sales.Wiz.COM命名了一个名为sales的 DNS 条目,相对于 DNS 条目Wiz.COM。而 DNS 条目Wiz.COM又命名了一个名为Wiz的条目,位于COM条目中。

轻量级目录访问协议(LDAP)命名约定按从右到左的顺序排列组件,以逗号字符(“,”)分隔。因此,LDAP 名称cn=Rosanna Lee, o=Sun, c=US指定了一个 LDAP 条目cn=Rosanna Lee,相对于条目o=Sun,后者又相对于c=us。LDAP 还有一个规则,即名称的每个组件必须是一个名称/值对,名称和值之间用等号字符(“=”)分隔。

绑定

名称与对象的关联称为绑定。文件名绑定到文件。

DNS 包含将机器名称映射到 IP 地址的绑定。LDAP 名称绑定到 LDAP 条目。

参考和地址

根据命名服务的不同,一些对象无法直接存储在命名服务中;也就是说,不能将对象的副本放入命名服务中。相反,它们必须通过引用存储;也就是说,将指向对象的指针引用放入命名服务中。引用表示如何访问对象的信息。通常,它是一种紧凑的表示形式,可用于与对象通信,而对象本身可能包含更多状态信息。使用引用,您可以联系对象并获取有关对象的更多信息。

例如,飞机对象可能包含飞机的乘客和机组人员列表,其飞行计划,燃料和仪表状态,以及其航班号和起飞时间。相比之下,飞机对象引用可能只包含其航班号和起飞时间。引用是关于飞机对象信息的更紧凑表示,并可用于获取附加信息。例如,文件对象使用文件引用访问。另外,打印机对象可能包含打印机的状态,如当前队列和纸盒中的纸张量。另一方面,打印机对象引用可能只包含有关如何到达打印机的信息,如其打印服务器名称和打印协议。

尽管一般来说引用可以包含任意信息,但将其内容称为地址(或通信终点)是有用的:关于如何访问对象的具体信息。

为简单起见,本教程在不需要区分两者时使用“对象”来指代对象和对象引用。

上下文

上下文是一组名称到对象的绑定。每个上下文都有一个相关的命名约定。上下文始终提供一个查找(解析)操作,返回对象,通常还提供绑定名称、解绑名称和列出绑定名称等操作。一个上下文对象中的名称可以绑定到另一个具有相同命名约定的上下文对象(称为子上下文)。

几个上下文示例,绑定到子上下文。

UNIX 文件系统中的文件目录,例如/usr,代表一个上下文。相对于另一个文件目录命名的文件目录代表一个子上下文(UNIX 用户将其称为子目录)。也就是说,在文件目录/usr/bin中,目录binusr的子上下文。DNS 域,例如COM,代表一个上下文。相对于另一个 DNS 域命名的 DNS 域代表一个子上下文。对于 DNS 域Sun.COM,DNS 域SunCOM的子上下文。

最后,LDAP 条目,例如c=us,代表一个上下文。相对于另一个 LDAP 条目命名的 LDAP 条目代表一个子上下文。对于 LDAP 条目o=sun,c=us,条目o=sunc=us的子上下文。

命名系统和命名空间

命名系统是同一类型上下文的连接集合(它们具有相同的命名约定),并提供一组共同的操作。

实现 DNS 的系统是一个命名系统。使用 LDAP 进行通信的系统也是一个命名系统。

命名系统为其客户提供命名服务,用于执行与命名相关的操作。命名服务通过其自己的接口访问。DNS 提供将机器名称映射到 IP 地址的命名服务。LDAP 提供将 LDAP 名称映射到 LDAP 条目的命名服务。文件系统提供将文件名映射到文件和目录的命名服务。

命名空间是命名系统中所有可能名称的集合。UNIX 文件系统具有一个命名空间,其中包含该文件系统中所有文件和目录的名称。DNS 命名空间包含 DNS 域和条目的名称。LDAP 命名空间包含 LDAP 条目的名称。

目录概念

原文:docs.oracle.com/javase/tutorial/jndi/concepts/directory.html

许多命名服务都扩展了目录服务。目录服务将名称与对象关联,并将这些对象与属性关联起来。

目录服务 = 命名服务 + 包含属性的对象

你不仅可以通过名称查找对象,还可以获取对象的属性或根据其属性搜索对象。

显示目录系统的图表:名称引用包含属性的目录对象。

一个例子是电话公司的目录服务。它将订户的姓名映射到他的地址和电话号码。计算机的目录服务非常类似于电话公司的目录服务,因为两者都可以用来存储诸如电话号码和地址之类的信息。然而,计算机的目录服务更加强大,因为它可以在线使用,并且可以用来存储各种信息,这些信息可以被用户、程序甚至计算机本身和其他计算机利用。

目录对象表示计算环境中的一个对象。例如,目录对象可以用来表示打印机、人员、计算机或网络。目录对象包含描述其所代表对象的属性

属性

目录对象可以具有属性。例如,打印机可以由具有其速度、分辨率和颜色等属性的目录对象表示。用户可以由具有用户的电子邮件地址、各种电话号码、邮寄地址和计算机帐户信息等属性的目录对象表示。

属性具有属性标识符和一组属性值。属性标识符是一个标记,独立于其值标识属性。例如,两个不同的计算机帐户可能具有一个"mail"属性;"mail"是属性标识符。属性值是属性的内容。例如,电子邮件地址可能有:

Attribute Identifier : Attribute Value
                 mail   john.smith@example.com

目录和目录服务

目录是一组连接的目录对象。目录服务是提供用于在目录中创建、添加、删除和修改与对象关联的属性的操作的服务。该服务通过其自己的接口访问。

可能有许多目录服务的示例。

网络信息服务(NIS)

NIS 是 UNIX 操作系统上可用的目录服务,用于存储与机器、网络、打印机和用户相关的系统信息。

Oracle 目录服务器

Oracle 目录服务器是一个基于互联网标准LDAP的通用目录服务。

搜索服务

你可以通过向目录服务提供其名称来查找目录对象。另外,许多目录,比如基于 LDAP 的目录,支持搜索的概念。当你搜索时,你可以提供一个查询,其中包含一个逻辑表达式,你可以在其中指定对象必须具有的属性。这个查询被称为搜索过滤器。这种搜索方式有时被称为反向查找基于内容的搜索。目录服务会搜索并返回满足搜索过滤器的对象。

例如,你可以查询目录服务以查找:

  • 所有具有属性"age"大于 40 岁的用户。

  • 所有 IP 地址以"192.113.50"开头的机器。

结合命名和目录服务

目录通常将它们的对象按层次结构排列。例如,LDAP 将所有目录对象排列在一棵树中,称为目录信息树(DIT)。在 DIT 中,一个组织对象,例如,可能包含可能又包含人员对象的组对象。当目录对象以这种方式排列时,它们除了作为属性容器外还扮演了命名上下文的角色。

课程:JNDI 概述

原文:docs.oracle.com/javase/tutorial/jndi/overview/index.html

Java 命名和目录接口(JNDI)是一个提供命名和目录功能的应用程序编程接口(API),用于使用 Java™ 编程语言编写的应用程序。它被定义为独立于任何特定目录服务实现。因此,各种目录 - 新的、新兴的和已部署的都可以以一种通用的方式访问。

架构

JNDI 架构包括 API 和服务提供者接口(SPI)。Java 应用程序使用 JNDI API 访问各种命名和目录服务。SPI 使各种命名和目录服务能够透明地插入,从而允许使用 JNDI API 的 Java 应用程序访问它们的服务。请参见下图:

JNDI 架构

打包

JNDI 包含在 Java SE 平台中。要使用 JNDI,您必须具有 JNDI 类和一个或多个服务提供者。JDK 包含以下命名/目录服务的服务提供者:

  • 轻量级目录访问协议(LDAP)

  • 公共对象请求代理体系结构(CORBA)公共对象服务(COS)名称服务

  • Java 远程方法调用(RMI)注册表

  • 域名服务(DNS)

其他服务提供者可以从JNDI 页面下载或从其他供应商获取。

JNDI 分为五个包:

  • javax.naming

  • javax.naming.directory

  • javax.naming.ldap

  • javax.naming.event

  • javax.naming.spi

课程的下一部分简要描述了 JNDI 包。

命名包

原文:docs.oracle.com/javase/tutorial/jndi/overview/naming.html

javax.naming包含用于访问命名服务的类和接口。

上下文

javax.naming包定义了一个Context接口,这是查找、绑定/解绑、重命名对象以及创建和销毁子上下文的核心接口。

查找

最常用的操作是lookup()。您提供lookup()要查找的对象的名称,它将返回绑定到该名称的对象。

绑定

listBindings()返回一个名称到对象绑定的枚举。绑定是一个包含绑定对象的名称、对象类的名称和对象本身的元组。

列表

list()类似于listBindings(),只是它返回一个包含对象名称和对象类名称的名称枚举。list()对于诸如浏览器之类的应用程序很有用,这些应用程序希望发现上下文中绑定的对象的信息,但不需要所有实际对象。虽然listBindings()提供了相同的信息,但它可能是一个更昂贵的操作。

名称

Name是表示通用名称的接口——一个有序的零个或多个组件的序列。命名系统使用此接口来定义遵循其约定的名称,如命名和目录概念课程中所述。

引用

对象以不同方式存储在命名和目录服务中。引用可能是对象的非常紧凑的表示。

JNDI 定义了Reference类来表示引用。引用包含有关如何构造对象副本的信息。JNDI 将尝试将从目录查找的引用转换为它们所代表的 Java 对象,以便 JNDI 客户端产生存储在目录中的是 Java 对象的错觉。

初始上下文

在 JNDI 中,所有命名和目录操作都是相对于上下文执行的。没有绝对的根。因此,JNDI 定义了一个InitialContext,它提供了命名和目录操作的起始点。一旦您有了初始上下文,您可以使用它来查找其他上下文和对象。

异常

JNDI 定义了一个类层次结构,用于在执行命名和目录操作过程中可能抛出的异常。这个类层次结构的根是NamingException。对于对特定异常感兴趣的程序,可以捕获异常的相应子类。否则,它们应该捕获NamingException

目录和 LDAP 包

原文:docs.oracle.com/javase/tutorial/jndi/overview/dir.html

目录包

javax.naming.directory包扩展了javax.naming包,提供了访问目录服务以及命名服务的功能。该包允许应用程序检索存储在目录中的对象相关的属性,并使用指定的属性搜索对象。

目录上下文

DirContext接口代表一个目录上下文DirContext还通过扩展Context接口来充当命名上下文。这意味着任何目录对象也可以提供命名上下文。它定义了用于检查和更新与目录条目关联的属性的方法。

属性

您可以使用getAttributes()方法检索与目录条目关联的属性(您提供名称)。使用modifyAttributes()方法修改属性。您可以使用此操作添加、替换或删除属性和/或属性值。

搜索

DirContext包含用于执行基于内容的目录搜索的方法。在最简单和最常见的用法中,应用程序指定一组可能具有特定值的属性,并将此属性集提交给search()方法。其他重载形式的search()支持更复杂的搜索过滤器。

LDAP 包

javax.naming.ldap 包含了用于使用特定于LDAP v3的功能的类和接口,这些功能不在更通用的javax.naming.directory包中。事实上,大多数使用 LDAP 的 JNDI 应用程序会发现javax.naming.directory包已经足够,并且根本不需要使用javax.naming.ldap包。这个包主要是为那些需要使用“扩展”操作、控件或未经请求的通知的应用程序而设计的。

“扩展”操作

除了指定了搜索和修改等明确定义的操作之外,LDAP v3 (RFC 2251)还指定了在 LDAP 客户端和服务器之间传输尚未定义操作的方法。这些操作被称为“扩展”操作。一个“扩展”操作可以由标准组织(如互联网工程任务组)或供应商定义。

控件

LDAP v3允许任何请求或响应通过尚未定义的修饰符进行增强,称为控件。与请求一起发送的控件是请求控件,与响应一起发送的控件是响应控件。控件可以由标准组织(如 IETF)或供应商定义。请求控件和响应控件不一定是成对的,也就是说,并非每个发送的请求控件都需要有相应的响应控件,反之亦然。

未经请求的通知

除了客户端和服务器之间的正常请求/响应交互方式之外,LDAP v3还指定了未经请求的通知--这些消息是从服务器异步发送给客户端的,而不是响应于任何客户端请求。

LDAP 上下文

LdapContext 接口代表了执行“扩展”操作、发送请求控件和接收响应控件的上下文。如何使用这些功能的示例在 JNDI 教程的控件和扩展课程中有描述。

事件和服务提供者包

原文:docs.oracle.com/javase/tutorial/jndi/overview/event.html

事件包

javax.naming.event包含用于支持命名和目录服务中事件通知的类和接口。事件通知在事件通知指南中有详细描述。

事件

一个NamingEvent代表由命名/目录服务生成的事件。事件包含一个类型,用于标识事件类型。例如,事件类型被分类为影响命名空间的事件,如“对象添加”,以及不影响命名空间的事件,如“对象更改”。

监听器

一个NamingListener是一个监听NamingEvent的对象。每种事件类型的类别都有相应类型的NamingListener。例如,一个NamespaceChangeListener代表一个对命名空间更改事件感兴趣的监听器,而一个ObjectChangeListener代表一个对对象更改事件感兴趣的监听器。

要接收事件通知,监听器必须注册到EventContextEventDirContext中。一旦注册,当命名/目录服务中发生相应更改时,监听器将接收事件通知。有关事件通知的详细信息可以在JNDI 教程中找到。

服务提供者包

javax.naming.spi包提供了不同命名/目录服务提供者的开发者可以开发和连接其实现的手段,以便从使用 JNDI 的应用程序中访问相应服务。

插件架构

javax.naming.spi包允许动态地插入不同的实现。这些实现包括初始上下文和可以从初始上下文访问的上下文。

Java 对象支持

javax.naming.spi包支持 lookup 及其相关方法的实现者返回对于 Java 程序员而言自然且直观的 Java 对象。例如,如果你从目录中查找打印机名称,那么你很可能期望得到一个打印机对象来进行操作。这种支持以 object factories 的形式提供。

这个包还提供了支持进行相反操作的功能。也就是说,Context.bind()及其相关方法的实现者可以接受 Java 对象,并将这些对象存储在底层命名/目录服务可接受的格式中。这种支持以 state factories 的形式提供。

多个命名系统(联邦)

JNDI 操作允许应用程序提供跨多个命名系统的名称。在完成操作的过程中,一个服务提供者可能需要与另一个服务提供者交互,例如将操作传递给下一个命名系统继续进行。这个包提供了不同提供者合作完成 JNDI 操作的支持。

有关服务提供者机制的详细信息可以在JNDI 教程中找到。

课程:软件设置

原文:docs.oracle.com/javase/tutorial/jndi/software/index.html

所需软件

以下是您需要的软件/系统列表:

  • Java 平台软件

  • 服务提供者软件

  • 命名和目录服务器软件


Java 平台软件

JNDI 包含在 Java SE 平台中。

要运行小程序,请在 Microsoft Edge 上使用 IE 模式。请参阅Microsoft Edge + Internet Explorer 模式:入门指南

服务提供者软件

JNDI API 是用于访问任何命名或目录服务的通用 API。通过在 JNDI 下插入服务提供者,可以实现对命名或目录服务的实际访问。有关 JNDI 架构和服务提供者角色的概述,请参阅 JNDI 概述课程。

服务提供者是将 JNDI API 映射到对命名或目录服务器的实际调用的软件。通常,服务提供者的角色与命名/目录服务器的角色不同。在客户端/服务器软件术语中,JNDI 和服务提供者是客户端(称为JNDI 客户端),而命名/目录服务器是服务器

客户端和服务器可以以许多方式进行交互。在一种常见的方式中,它们使用网络协议,以便客户端和服务器可以在网络环境中独立存在。服务器通常支持许多不同的客户端,不仅限于 JNDI 客户端,只要客户端符合指定的协议。JNDI 不规定 JNDI 客户端和服务器之间的任何特定交互方式。例如,在一个极端情况下,客户端和服务器可以是同一实体。

您需要获取将要使用的服务提供者的类。例如,如果您计划使用 JNDI 访问 LDAP 目录服务器,则需要 LDAP 服务提供者的软件。

JDK 附带以下服务提供者:

  • 轻量级目录访问协议(LDAP)

  • CORBA 公共对象服务命名(COS 命名)

  • RMI 注册表

  • 域名服务(DNS)

如果您对其他提供者感兴趣,请查看JNDI 页面以获取下载信息。

本教程仅使用 LDAP 服务提供者。使用 LDAP 服务提供者时,您需要设置自己的服务器或访问现有服务器,如下所述。

命名和目录服务器软件

一旦您获得了服务提供商软件,您就需要设置或访问相应的命名/目录服务器。设置命名/目录服务器通常是网络系统管理员的工作。不同的供应商对其命名/目录服务器有不同的安装程序。有些在服务器安装之前需要特殊的机器权限。您应该查阅命名/目录服务器软件的安装说明。

在本教程中的目录示例中,您需要访问一个 LDAP 服务器。如果您想快速了解 LDAP 是什么,请查看这里。您可以使用您选择的任何符合 LDAP 标准的服务器。Oracle Directory Server 在许多平台上运行,包括 Windows,可在以下网址进行评估:Oracle Directory Server

您也可以在下面下载一个免费的 LDAP 服务器:

一个公开访问的服务器位于:ldap://ldap.openldap.org 命名上下文:dc=OpenLDAP,dc=org

LDAP 设置

原文:docs.oracle.com/javase/tutorial/jndi/software/content.html

下面是构建访问 LDAP 目录服务器的 Java 应用程序涉及的步骤。

  1. 安装 Java 平台软件。

  2. 获取目录服务器软件,如之前讨论过的。

  3. 配置目录服务器以使用所需的模式。在本教程中使用的示例需要在服务器上配置一个特殊的 schema。

  4. 使用所需的内容填充目录服务器。在本教程中使用的示例需要在服务器上填充一个特殊的内容。

  5. 编写一个 JNDI 应用程序来访问目录,编译并运行它以获得您想要的结果。JNDI 示例将在下一个课程中介绍。

前两个步骤在前一部分中已经涵盖。本课程的其余部分讨论了第三步第四步的一部分。涉及编写 JNDI 应用程序的第五步在下一课程中介绍,展示如何编写 JNDI 应用程序来执行目录上的各种操作。

一旦您设置了目录,或者已经指示您的程序与现有目录通信,您可以在那里找到什么样的信息?

目录可以被视为由名称到对象绑定组成。也就是说,目录中的每个对象都有一个对应的名称。您可以通过查找其名称来检索目录中的对象。

目录中还存储着属性。目录中的对象除了有一个名称外,还有一组可选的属性。您可以向目录查询对象的属性,也可以要求它搜索具有特定属性的对象。

步骤 3:目录模式

模式指定目录可能包含的对象类型。本教程使用条目填充目录,其中一些条目需要特殊的模式定义。为了容纳这些条目,您必须首先在服务器中关闭模式检查,或者将附带本教程的模式文件添加到服务器中。这两项任务通常由目录服务器的管理员执行。

本教程附带两个必须安装的模式文件:

  • Java 对象的模式

  • CORBA 对象的模式

这些文件的格式是一种可能不能直接复制粘贴到服务器配置文件中的正式描述。具体来说,属性语法是根据RFC 2252描述的。

不同的目录服务器有不同的配置模式方式。本教程包括一些工具,用于在允许通过 LDAP 修改其模式的目录服务器上安装 Java 和 CORBA 模式。以下是工具可以执行的任务列表。

  1. 创建 Java 模式

  2. 创建 CORBA 模式

按照随附的README 文件中的说明运行这些程序。


注意:Windows 活动目录。 Active Directory 通过使用内部格式管理其模式。要更新模式,您可以使用 Active Directory 管理控制台插件ADSIEditCreateJavaSchema实用程序,按照 Active Directory 的说明进行操作。


第 4 步:为本教程提供目录内容

在本教程的示例中,显示的结果反映了使用随附本教程的配置文件(tutorial.ldif)设置 LDAP 目录的方式。如果您正在使用现有服务器或具有不同设置的服务器,则可能会看到不同的结果。在将配置文件(tutorial.ldif)加载到目录服务器之前,您必须按照更新服务器模式的说明进行操作,或者如果您的 UNIX 系统上有ldapaddldapmodify命令,则可以使用它们。

例如,使用 ldapmodify,您可以执行以下操作(通过为主机名、管理员 DN(-D 选项)和密码插入适当的值):

ldapmodify -a -c -v -h hostname -p 389\
        -D "cn=Administrator, cn=users, dc=xxx, dc=xxx"\
        -w passwd -f tutorial.ldif


安装注意事项:访问控制。 不同的目录服务器以不同方式处理访问控制。本教程中的一些示例执行对目录的更新。此外,您安装教程的命名空间部分可能具有读取访问限制。因此,您需要采取特定于服务器的操作,使目录可读和/或可更新,以使这些示例正常工作。对于Oracle 目录服务器,请将sunds.aci.ldif文件中建议的aci条目添加到dn: o=JNDITutorial条目中,以使整个目录可读和可更新。或者,您可以更改示例以对目录进行身份验证。如何执行此操作的详细信息在安全课程中有描述。

安装注意事项:命名空间设置。 tutorial.ldif 文件中的条目使用了"o=JNDITutorial"作为根命名上下文的区分名称(DN)。如果您尚未将目录服务器配置为具有"o=JNDITutorial"作为根命名上下文,则导入tutorial.ldif的尝试将失败。解决此问题的最简单方法是将现有根命名上下文的 DN 添加到tutorial.ldif文件中的每个"dn:"行中。例如,如果您的服务器已经具有根命名上下文"dc=imc,dc=org",则应更改该行

dn: o=JNDITutorial

dn: o=JNDITutorial, dc=imc, dc=org

对文件中以"dn:"开头的每一行进行更改。然后,在本教程的所有示例中,无论何时使用"o=JNDITutorial",请改用"o=JNDITutorial,dc=imc,dc=org"。

安装说明:文件格式。根据您使用的操作系统平台,您可能需要编辑tutorial.ldif,以便其中包含该平台的正确换行符。例如,如果您发现tutorial.ldif包含 Windows 风格的换行符(CRLF),而您要将此文件导入运行在 UNIX 平台上的目录服务器,则需要编辑文件并将 CRLF 替换为 LF。此问题的症状是目录服务器拒绝tutorial.ldif中的所有条目。

安装说明:Windows Active Directory。

  1. 根命名上下文不会是“o=jnditutorial”。它将采用“dc=x,dc=y,dc=z”的形式。您需要遵循之前的命名空间设置说明。

  2. 通过使用 Active Directory 管理控制台插件ADSIEdit,为“inetOrgPerson”和“groupOfUniqueNames”对象类添加对象类和相关属性到 Active Directory 模式中。"groupOfUniqueNames"在RFC 2256中定义,"inetOrgPerson"在RFC 2798中定义。

  3. 教程中使用的一些层次关系在 Active Directory 中默认情况下是不允许的。要启用这些关系,请使用 Active Directory 管理控制台插件ADSIEdit添加它们。

    objectclass: organizationalUnit
    possible superiors: domainDNS
                        inetOrgPerson
                        organization
                        organizationalPerson
                        organizationalUnit
                        person
                        top
    
    objectclass: groupOfUniqueNames
    possible superiors: top
    
    objectclass: inetOrgPerson
    possible superiors: container
                        organizationalPerson
                        person
                        top
    
    
  4. tutorial.ldif 中的马克·吐温条目中删除两个“sn”属性中的一个。Active Directory 将“sn”定义为单值属性,与RFC 2256相悖。

  5. 使用ldifde命令行实用程序加载修改后的tutorial.ldif文件。

    # ldifde -i -v -k -f tutorial.ldif
    
    
  6. 大多数示例假定目录已设置为允许未经身份验证的读取和更新访问。您的 Active Directory 设置可能不允许您这样做。请参阅访问控制安装说明。

  7. 有时读取条目会产生比教程中显示的更多属性,因为 Active Directory 通常会返回一些内部属性。

  8. 创建条目可能需要指定其他 Active Directory 特定属性或使用其他对象类。

Java 应用程序设置

原文:docs.oracle.com/javase/tutorial/jndi/software/package.html

要在程序中使用 JNDI,您需要设置其编译和执行环境。

导入 JNDI 类

以下是 JNDI 包:

本教程中的示例使用来自前两个包的类和接口。您需要将这两个包导入到您的程序中,或者导入您使用的单个类和接口。以下两行导入了两个包javax.namingjavax.naming.directory中的所有类和接口。

import javax.naming.*;
import javax.naming.directory.*;

编译环境

要编译使用 JNDI 的程序,您需要访问 JNDI 类。 Java SE 6 已经包含了 JNDI 类,因此如果您正在使用它,您无需采取进一步的操作。

执行环境

要运行使用 JNDI 的程序,您需要访问 JNDI 类和程序使用的任何服务提供商的类。 Java Runtime Environment(JRE)6 已经包含了 LDAP、COS 命名、RMI 注册表和 DNS 的 JNDI 类和服务提供商。

如果您正在使用其他服务提供商,则需要下载并安装它们的存档文件到JAVA_HOME/jre/lib/ext目录中,其中JAVA_HOME是包含 JRE 的目录。 JNDI 页面列出了一些服务提供商。您可以下载这些提供商或使用其他供应商的提供商。

课程:命名和目录操作

原文:docs.oracle.com/javase/tutorial/jndi/ops/index.html

您可以使用 JNDI 执行命名操作,包括读取操作和更新命名空间的操作。本课程中描述了以下操作:

  • 查找对象

  • 列出上下文的内容

  • 添加、覆盖和删除绑定

  • 重命名对象

  • 创建和销毁子上下文

配置

在执行任何命名或目录服务操作之前,您需要获取一个初始上下文--进入命名空间的起始点。这是因为所有命名和目录服务上的方法都是相对于某个上下文执行的。要获取初始上下文,您必须按照以下步骤进行。

  1. 选择要访问的相应服务的服务提供者。

  2. 指定初始上下文需要的任何配置。

  3. 调用InitialContext构造函数。

步骤 1:选择初始上下文的服务提供者

您可以通过创建一组环境属性Hashtable)并将服务提供者类的名称添加到其中来指定初始上下文要使用的服务提供者。环境属性在JNDI 教程中有详细描述。

如果您正在使用 JDK 中包含的 LDAP 服务提供者,则您的代码将如下所示。

Hashtable<String, Object> env = new Hashtable<String, Object>();
env.put(Context.INITIAL_CONTEXT_FACTORY, 
        "com.sun.jndi.ldap.LdapCtxFactory");

要在 JDK 中指定文件系统服务提供者,您需要编写类似以下代码的代码。

Hashtable<String, Object> env = new Hashtable>String, Object>();
env.put(Context.INITIAL_CONTEXT_FACTORY, 
        "com.sun.jndi.fscontext.RefFSContextFactory");

您还可以使用系统属性来指定要使用的服务提供者。查看JNDI 教程获取详细信息。

步骤 2:提供初始上下文所需的信息

不同目录的客户端可能需要各种信息来联系目录。例如,您可能需要指定服务器在哪台机器上运行以及需要什么信息来识别用户到目录。此类信息通过环境属性传递给服务提供者。JNDI 指定了一些通用的环境属性,服务提供者可以使用。您的服务提供者文档将详细说明这些属性所需的信息。

LDAP 提供程序要求程序指定 LDAP 服务器的位置,以及用户身份信息。为了提供这些信息,您需要编写如下代码。

env.put(Context.PROVIDER_URL, "ldap://ldap.wiz.com:389");
env.put(Context.SECURITY_PRINCIPAL, "joeuser");
env.put(Context.SECURITY_CREDENTIALS, "joepassword");

本教程使用 JDK 中的 LDAP 服务提供程序。示例假设在本地机器上的端口 389 上设置了一个服务器,根专有名称为"o=JNDITutorial",并且更新目录不需要认证。它们包括以下代码来设置环境。

env.put(Context.PROVIDER_URL, "ldap://localhost:389/o=JNDITutorial");

如果你正在使用设置不同的目录,那么你需要相应地设置这些环境属性。你需要将"localhost"替换为那台机器的名称。你可以运行这些示例代码来访问任何公共目录服务器或者运行在不同机器上的你自己的服务器。你需要将"localhost"替换为那台机器的名称,并将o=JNDITutorial替换为可用的命名上下文。

第三步:创建初始上下文

现在你已经准备好创建初始上下文了。为此,你需要将之前创建的环境属性传递给InitialContext构造函数

Context ctx = new InitialContext(env);

现在你已经有了一个指向Context对象的引用,你可以开始访问命名服务。

要执行目录操作,你需要使用一个InitialDirContext。为此,使用其中的一个构造函数

DirContext ctx = new InitialDirContext(env);

这个语句返回一个用于执行目录操作的DirContext对象的引用。

posted @ 2024-04-12 15:14  绝不原创的飞龙  阅读(3)  评论(0编辑  收藏  举报