Lecture 3 提到了可变的数组(Resizable Array),LinkedList类。
这节课详细讲哈希表(Hash Table)接口。
.hashCode()方法返回一串字符如:"hash = 1550089733"
可以用一个模组,将对象存储在特定的位置。
但是不同的元素(items)可以有同样的hashCode,这意味着它们被放在了同一个地方。这个问题叫作冲突(conflict)。
自动的解决方案就是在每个数组槽,用linked list对其中元素进行散列。那么,一个哈希表的结构也就显而易见了。它是由list和array的组合。如果设计得当,那么它将会同时有array和list的特点。
HashMap就是使用这样的结构来存储一对对象。其中元素的hashCode()返回的就是它对应的键(Key)。
由于HashMap中的键都是特殊的,这就符合集合的特点,因此,HashMap可以返回一个集合。这样,就可以使用一个迭代器来遍历这个集合。这个迭代器叫作表迭代器(Map Iterator)。
示例代码 :
import java.util.*;
Set set = hm.entrySet();
Iterator it = set.iterator();
while(it.hasNext()){
Map.Entry en = (Map.Entry)it.next();
System.out.println("key: " + en.getKey() + ", value: " + en.getValue() ) ;
}
也可以用foreach直接调用迭代器,而不用Map iterator:
for(Map.Entry en : set)
System.out.println("key: " + en.getKey() + ", value: " + en.getValue() ) ;
HashMap非常适合动态的数据,也具有很高的查询效率。但是,HashMap内的元素,没有有序性(order),也没有时间信息(chronology)(除非存储时间信息到元素内)。
哈希集合(HashSets)相当于集合实现了HashMaps的接口。因为键只出现一次,一个HashMap也可以实现集合的接口。
链式哈希表(LinkedHashMap)可以返回一列以插入顺序同序的对象,当被迭代时,情况会和HashMap不一样。
最后的主要的接口是树(Tree):
树的结构与链表(list)相似,上一个元素带着其下元素的地址,但一个元素可以有多个地址指向多个元素。查找时分支的选择,取决于如何比较(compare)子节点(node or left),查找效率与二分查找一样。
容器会使树保持平衡(balanced),也就是其中一个分支不会比另一个分支大太多。
树的查找基于强排序(Strongly ordered)(键实现了Comparable接口)和背后以递归形式工作的迭代器。树结合了哈希表的高效查找和链表的强排序。
TreeMaps(树表):
当数据是动态化时适用
高效查找
结构排序
无时间信息 (像哈希表一样,树不会记录元素的插入时间)
由于接口的使用要求和接口的限制,一些组合结果会非常管用,而另一些组合却不那么合适。例如,因为哈希表和树都不记录插入时间,那么他们实现(implement)Queue或Dueue是没有意义的。相反,数组(arrays)和列表(lists)就非常适合相互实现。当图(maps)和集合(sets)相互实现时,它的查找性能很好,相反,数组和列表却不那么管用,哈希表和树的结合就比较出色。需要怎样的组合都由你的需求而决定。
一个关于哈希表的有趣的例子就是Properties类, 它与两个字符串有关。
java.util.Properties
当你安装一些程序到你的电脑时,你通常会需要确定一些信息,例如程序安装位置,数据的存储位置和界面风格。这些信息都存储在一个后缀为 .ini 或 .conf 或其他后缀的文件。所有的参数都是一个与值(Value)有关的名字。Properties对象会处理这些文件,读写,以及忽略掉以#开头的行。
例子 : preferences.cnf
# Location of data files
data_dir = C:\Users\Public\Data
# Remove server
server = 192.168.1.214
#Theme name
theme = Funky
示例代码 :
import java.io.BufferedReader ;
import java.io.FileReader ;
import java.io.FileReader ;
import java.util.Properties ;
public class PropertiesExample {
public static void main(String args[] ){
Properties defprop = new Properties() ;
defprop.put("data_dir", " ");
defprop.put("theme", "classic");
Properties prop = new Properties(defprop) ;
try(BufferedReader conf
= new BufferedReader(new FileReader("preferences.cnf"))){
prop.load(conf);
} catch( IOException e) {
System.out.println("Waring: using default preferences") ;
}
System.out.println(prop.getProperty("data_dir")) ;
System.out.println(prop.getProperty("theme")) ;
}
}
示例 #2 :
使用Tokenizer类在文章中返回单词,它可以自动忽略空格和标点符号。目标是找出一篇文章中,使用次数最多的前十个词。所以我们需要把每一个词都和一个计数器连接,那么我们就需要有Map这样的东西。在读取每一个词时,如果判断到这个词是没有记录的,就存储这个词,并设置计数器为1,如果是有记录的,就使计数器加一。当我们读取完所有单词时,我们需要找出出现次数最多的十个词。我们需要一个表(map)来存储这些词,但是我们又要求这些词是有序的。这就需要一个hashMap来存储对应的对象,例如,使用TreeMap>, 然后,我们迭代树表返回最常见的词。
返回的结果通常是令人失望的,因为在一篇英语文章中,最常见的词就是"the", "is", "a"之类的。这些词都是没有意义的,通常其被称为"停用词"(stop words)(因特网的搜索引擎会忽略这些词)。
需要做的是创建一个含有停用词的列表,将其存储在一个易于查找的数据结构,例如树,然后我们在对文章的词计数时,先查找这个词是否在这个列表内,如果没有,才进行计数。
:
Annotations (标注)
第一个功能是Annotations,你也许会注意到一些annotations。重定义一个方法继承的子类,子类的前面会有 @Override (表示替换了之前存在的方法)。这就是一个annotation,它不一定要在代码中写出来的但是它会告诉javac你的目的是什么。如果你输入错了变量或方法的名字,javac会告诉你父类中没有这个变量或方法。
Annotations有以下几个特点:
完全是可选的
不会对程序行为造成任何影响
帮助javac更好地识别代码
多用在代码生成工具
既然annotations可以被程序访问,许多工具可以生成代码。例如,使用annotations去收集一些无法它们无法获得的信息[标注 3?]
元数据(METADATA, = DATA about the CODE)
元数据是现实生活中的一大顾虑。公司把程序看作资产,这些资产可以为几代开发者服务,因此代码应该以一种易于理解(easy-to-comprehend)的,标准的(standard way),易读写的(well documented)的方式来编写。元数据允许在许多东西中,使代码工业化和标准化。
三种标准的annotations :
@Override
@Deprecated
@SuppressWarnings(warnings to support)
例如 : @SuppressWarnings({"deprecation","unchecked"})
两种在Java7和Java8新增的annotations
@SafeVarargs
@FunctionalInterface
创造自己的annotations
像声明interfaces一样 :
import java.lang.annotation.* ;
public @interface MyAnnotation {}
annotations可以有方法,但不能有任何参数,也不能抛出任何东西。
返回类型必须是其中之一或其数组 :
primitive type
String
enum
Class
boolean int char float double...
import java.lang.annotation.* ;
public @interface ClassDoc{
String author() ;
String created() ;
String[] revisions() ;
}
@ClassDoc(author= "Ezreal_Serious", created= "2018/04/16", revisions= {"2018/01/21 - Constructor withe String parameter", "2018/3/28 - toString() rewritten"} )
class SomeClass{ }
五种其他与annotations有关的annotations (meta annotations)
@Retention 说明annotation是否可被javac访问,或在编译时访问
@Documented 用javadoc在docs中生成
@Target 用在构造器,方法,参数...
@Inherited 传递给子类(默认关闭)
@Repeatable 可以多次使用
JUNIT是一个会生成一个检查程序的测试,Frameworks是一个会自动生成一些无聊的代码的工具。
Reflection(反射机制)
程序可以访问annotations,当annotation的前缀是@Retention(RetentionPolicy.RUNTIME)时启动反射机制。
通俗地说,反射机制就是你的程序问java虚拟机知道些什么,而java虚拟机知道的事情可多了。
当程序正在运行时,它允许大量的动态操作,当然,如果程序是用C语言写的,就不可能。反射机制被认为是一项被经常使用的功能,而不是先进的编程习惯,如JDBC(标准的
反射机制就是检查(examine)和修改(modify)编译行为。
它由。
有两种从java虚拟机获取类信息的方式 :
ClassName obj = new ClassName() ;
obj.getClass() ; 实例化对象,获得信息 : “动态获取”
ClassName.class ; 静态获取
注意 : 无构造器参与这个过程,对象由java虚拟机建立。
示例代码 :
class OuterClass {
private int dummy ;
OuterClass(){}
}
public class MyClass{
class InnerClass{
private int dummy;
InnerClass(){}
}
public static void main(String[] args){
OuterClass obj = new OuterClass() ;
System.out.println(obj.getClass().getName() ) ;
System.out.println(InnerClass.class.getName() ) ;
}
}
$ java MyClass
OuterClass
MyClass$InnerClass
$
一些使用反射机制的有用例子
一 :
找到程序读取的文件位置:Parameter file(参数文件), data file(数据文件), multimedia(多媒体文件) 等。
背景 :
当可以用快捷方式打开程序时,"当前目录"的概念就变得十分模糊。如果想要从读取properties文件开始,或想要在程序初始化时显示公司的logo,那么应该在哪看呢?
不同操作系统的安装程序的默认路径是不同的,进一步说,用户也可以自定义安装路径。你只需要在需要运行程序时找到它就可以了。
解决方案 :
public class Refection {
public static void main(String[] args) {
System.out.println(Reflection.class.getClassLoader()
.getResource("Reflection.class").toString) ;
}
}
//~ file : /Users/... ... .../Reflection.class
如果你知道你需要的文件的层级,你可以很容易找到任何你提供的文件的地址。
URL url = this.getClass().getClassLoader().getResource("resources/images/myCat.png") ;
二 :
读取annotations :
背景 : annotation必须运行时可访问(用Rentention声明)。记住@Retention()是meta-annotation,是一种用于annotation的annotation。
解决方案 :
import java.lang.annotation.* ;
@Retention(RetentionPolicy.RUNTIME) ;
public @interface ClassDoc{
String author();
String created() ;
String[] revisions() ;
}
@ClassDoc(author= "Ezreal_Serious", created= "2018/04/16", revisions= {"2018/01/21 - Constructor withe String parameter", "2018/3/28 - toString() rewritten"} )
class SomeClass{ }
如果annotation有声明在运行时可访问,可以用getAnnotations()获取它
import java.lang.annotation.Annotation ;
public class ReadingAnnotations {
public static void main(String[] args){
Annotation[] annotation = someClass.class.getAnnotations() ;
for (Annotation annot : annotations)
System.out.println(annot.toString());
}
}
$ java ReadingAnnotations
@ClassDoc(author= Ezreal_Serious, created= 2018/04/16, revisions=[ 2018/01/21 - Constructor withe String parameter, 2018/3/28 - toString() rewritten])
$
三 :
动态加载类(Dynamically loading a class) :
背景 : 通常用在驱动。因为标准的多样化,同样的函数通常被不同的,使用相同的硬件或软件的类实现。这对访问数据库有特殊的帮助。尽管只有一种通用的语言来访问数据库,数据库还是提供了支持类来实现需要的方法,以此来与它们的系统建立沟通。通常驱动会有一个复杂的名字来保证不会发生命名冲突(com.company.whatever.Driver.class)。如果一个.jar文件的名称包括了CLASSPATH(加载器加载.class文件的地方),那么程序可以加载其驱动。
解决方案 :
Class c = Class.forName("com.company.whatever.Driver") ;
Driver drv = (Driver)c.newInstance() ;
有一个用java开发的软件叫Squirrel SQL的,只要有合适的.jar文件添加到CLASSPATH中,就可以查询几乎所有数据库。
Lambda 表达式
Lambda表达式和函数式编程紧密联系。
嵌套类(Nested Classes)
Class OuterClass{
private int attr ;
class NestedClass{
. . . .
}
}
有两种创建嵌套类对象的方法,具体使用的方法取决于嵌套类是否静态。
OuterClass.NestedClass nestedObject = outerObject.new NestedClass() ;
//取决于是否存在OuterClass对象
OuterClass.StaticNestedClass nestedObject = new OuterClass.StaticNestedClass() ;
//与是否存在OuterClass对象无关
嵌套类的目的是封装代码
也可以有一个局部类(Local Classes),局部类定义在一个方法内。
public void doSomething() {
class LocalClass{
. . .
}
}
复习一些知识
Interfaces:
抽象的。
定义一些实现时必须符合的方法。
没有变量属性。
可以有常量。
实现接口的唯一问题就是必须重写这些方法。
匿名类(Anonymous Classes)
通常我们只对接口的方法感兴趣,那么类名就变得毫无用处,例如Comparator对象,我们通常只需要它的comparaTo()方法。
Class NamedClass implements Interface{
. . .
}
Interface anObject = new NamedClass( . . . ) ;
Java允许定义一个无名的实现所需要接口的对象
Interface anObject = new Interface{
// 定义属性和方法
};
这不但对接口管用,还对继承管用,子类可以被命名
class NamedClass extends ParentClass{
. . . .
}
NamedClass anObject = new NamedClass( . . . ) ;
或者子类也可以不被命名
ParentClass anObject = new ParentClass() {
// 定义属性和方法
}