Stéphane's Java course : Lecture 4

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

@Sa​feVarargs

@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() {

                                  // 定义属性和方法

}

猜你喜欢

转载自blog.csdn.net/weixin_40804987/article/details/80033082