CH-01 绪论

学科介绍

软件工程

  • 2012年升级为教育部一级专业
  • 软件工程是指大型复杂计算机软件系统设计、开发、测试、维护的工程学科。

培养全栈工程师

  • 掌握软件开发过程的全链路技术
  • 注重实操
  • 不局限一门语言,重点学会编程思维
  • 使用编程解决生活中的一些问题
  • 对算法和数据结构有简单了解

教材

《程序设计基础(第四版)》 - 清华大学出版

主要章节

  1. 绪论
  2. 编程准备
  3. 代数思维与计算机解题
  4. 逻辑思维与计算机解题
  5. 函数思维与模块化设计
  6. 数据的组织与处理(1)- 数组
  7. 数据的组织与处理(2)- 结构
  8. 数据的组织与处理(3)- 文件
  9. 递归思想和相应算法

次要章节(有时间就讲的内容)

  1. 多步决策问题
  2. 宽度优先搜索
  3. 深度优先搜索
  4. 贪心法
  5. 动态规划
  6. 蒙特卡罗法

附录

A. 程序调试
B. 库函数
C. ASCII码表
D. 输入输出的格式控制

要求

  • 经过这门课程的学习,对于简单的程序,应可以做到纸笔编程, 学会快速构思程序结构, 积累经验避开常见的“坑”
  • 对于稍微复杂的程序使用IDE编程, 需要掌握一般调试手段。

考核

本门课程为理论课程,另外有实验课安排。

  • 平时成绩 40% - 考勤、答疑、测试、作业
  • 期末考试 60% - 机考 或者 试卷 (试题为标准C语言)

行业分析

IT行业岗位主要分为研发岗、市场岗、管理岗、技术支持岗。

研发岗位

对于软件开发的研发岗来说,又可细分为:

  • RD
    Research and Development engineer,研发工程师,对某种不存在的事物进行系统的研究和开发并具有一定经验的专业工作者,或者对已经存在的事物进行改进以达到优化目的的专业工作者。
  • PM
    Product Manager,产品经理,又称品牌经理。举凡产品从创意到上市,所有相关的研发、调研、生产、编预算、广告、促销活动等等,都由产品经理掌控。
  • QA
    Quality Assurance,品质保证。QA的主要职责就是质量保证工作。
  • OP
    Operator,运维操作员,服务器、数据库管理员。

file

其中由于产品经理与设计、开发工程师之间的恩恩怨怨,被编成了很多段子在网络上流传。
file

编程语言分类

最早的编程语言以Fortran为主,主要是辅助科学计算。随着技术更迭至今已经产生了上千种编程语言,
file
编程语言分为三类:

机器语言

  • 机器语言由很多0和1构成的指令组成,无需解释或编译计算机就可以直接执行
  • 编写难度大、效率低
  • 华为、Intel、高通、三星等研发芯片部门

汇编语言

  • 汇编语言的实质和机器语言是相同的,都是直接对硬件操作,只不过指令采用了英文缩写的标识符,更容易识别和记忆。下面是汇编版hello world程序,是不是十分复杂呢。
    ; hello.asm 
    section .data            ; 数据段声明
        msg db "Hello, world!", 0xA     ; 要输出的字符串
        len equ $ - msg                 ; 字串长度
    section .text            ; 代码段声明
    global _start            ; 指定入口函数
    _start:                  ; 在屏幕上显示一个字符串
        mov edx, len     ; 参数三:字符串长度
        mov ecx, msg     ; 参数二:要显示的字符串
        mov ebx, 1       ; 参数一:文件描述符(stdout) 
        mov eax, 4       ; 系统调用号(sys_write) 
        int 0x80         ; 调用内核功能
                         ; 退出程序
        mov ebx, 0       ; 参数一:退出代码
        mov eax, 1       ; 系统调用号(sys_exit) 
        int 0x80         ; 调用内核功能
  • 编写难度仍然很大,难理解和复用
  • 优点是汇编生成的可执行文件小、执行速度快
  • 芯片公司或硬件厂商

高级语言

高级语言是大多数编程者的选择。和汇编语言相比,它不但将许多相关的机器指令合成为单条指令,并且去掉了与具体操作有关但与完成工作无关的细节,例如使用堆栈、寄存器等,这样就大大简化了程序中的指令。同时,由于省略了很多细节,编程者也就不需要有太多的专业知识。

但是高级语言所编制的程序不能直接被计算机识别,必须经过转换才能被执行,按转换方式可将它们分为两类:

  1. 编译类

    是指在应用源程序执行之前,就将程序源代码“翻译”成目标代码(机器语言),因此其目标程序可以脱离其语言环境独立执行(编译后生成的可执行文件,是cpu可以理解的2进制的机器码组成的),使用比较方便、效率较高。但应用程序一旦需要修改,必须先修改源代码,再重新编译生成新的目标文件(* .obj,也就是OBJ文件)才能执行,只有目标文件而没有源代码,修改很不方便。
    编译后程序运行时不需要重新翻译,直接使用编译的结果就行了。程序执行效率高,依赖编译器,跨平台性差些。如C、C++、Delphi等

  2. 解释类

    执行方式类似于我们日常生活中的“同声翻译”,应用程序源代码一边由相应语言的解释器“翻译”成目标代码(机器语言),一边执行,因此效率比较低,而且不能生成可独立执行的可执行文件,应用程序不能脱离其解释器(想运行,必须先装上解释器,就像跟老外说话,必须有翻译在场),但这种方式比较灵活,可以动态地调整、修改应用程序。如Python、Javascript、PHP、Ruby等语言。

现在很多高级语言同时兼顾编译和解释型语言的特点。比如Java为了兼顾跨平台和运行速度,Java 源代码首先会被编译为字节码文件(.class),但并非是机器语言,而是需要在 JVM 上运行,而 .class 文件在 JVM 上是解释执行的。所以 Java 兼具编译型语言和解释型语言的特点,为了更高的效率,JVM 还引入了 JIT(just-in-time,即时编译)编译器,在 Java 程序运行时进一步编译,转换成高度优化的机器代码。由于Java更接近编译型语言的特征,个人更愿意称Java为编译型语言.

下图分别是编译型语言、解释型语言、以及基于虚拟机的语言。

file

面向过程 VS 面向对象

高级语言还可以分为面向过程和面向对象两种.

面向过程的语言

  • C / Fortran / Pascal / SQL
  • 把重复逻辑封装成函数
  • 控制结构构造: 顺序,选择,循环
  • 对于简单的程序, 流程明确, 简单粗暴高效, 但不利于编写复杂系统
  • 代码重用性低,扩展能力差,后期维护难度比较大

面向对象的语言

  • C++ / C# / Java / Python / Scala
  • 抽象共同属性和行为封装成类, 万物皆对象
  • 类与类之间关系更符合真实世界特征, 符合人类思维习惯
  • 使用类组织代码, 结构化, 模块化
  • 易扩展,代码重用率高,可继承,可覆盖,可以设计出低耦合的系统;
  • 易维护,系统低耦合的特点有利于减少程序的后期维护工作量。
  • 执行效率通常没有面向过程的语言高。

很多语言比如Javascript和PHP一开始是面向过程的, 后面也逐渐支持面向对象了.

这里所说的某个语言是面向过程或者是面向对象的, 这是通常情况下使用这个语言编写出来的代码是面向过程或者是面向对象的. 面向过程和面向对象更多是编程思想的差异, 并不是使用支持面向对象的语言比如Java写出来的就一定是面向对象的代码, 反过来使用纯C语言也不见得只能写出面向过程的代码, 只不过使用C语言编写面向过程的代码相对容易, 而使用Java语言编写面向对象代码相对容易, 因为Java是天生支持面向对象的语言.

国产编程语言

  • 易语言(中文写代码)
    file
  • 木兰(假)
    file

搞笑篇

下面这些并不算真正的编程语言, 只是好玩:

  • 文言文编程
    file w:30cm

  • 粤语编程
    w

面向交互的高级语言

  • HTML
  • Javascript
  • XML
  • ASP
  • PHP

编程语言排行榜

TIOBE编程语言排行榜是编程语言流行趋势的一个指标,每月更新,这份排行榜排名基于互联网有经验的程序员、课程和第三方厂商的数量。排名使用著名的搜索引擎(诸如Google、MSN、Yahoo!、Wikipedia、YouTube以及Baidu等)进行计算。请注意这个排行榜只是反映某个编程语言的热门程度,并不能说明一门编程语言好不好,或者一门语言所编写的代码数量多少。

这个排行榜可以用来考查你的编程技能是否与时俱进,也可以在开发新系统时作为一个语言选择依据。

C/C++如果联合起来毫无疑问会排到第一名. 只要计算机还是冯诺依曼体系, C语言将会一直永存.可以说C语言是最接近底层的高级语言, 执行效率最高.

反过来C/C++由于其复杂性不利于编写大型企业级网站项目, 因为内存必须程序员手动管理, 指针用起来也比较容易出错. 而Java语言没有指针类型, 又支持垃圾回收(GC)机制, 更容易编写出健壮的程序.

如何成为大神

  • 多敲多练多想 大神如何敲代码
  • 云笔记多总结
    • 推荐有道云笔记
    • 及时偿还技术债
  • 善用搜索引擎
    • 谷歌/百度
  • 学会提出问题
    • 切忌无脑问
    • 说明前因后果
    • 总结经验

Views: 52

JSON简介

JSON简介

JSON 是一种与开发语言无关的、轻量级的数据格式 - JavaScript Object Notation

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。 易于人阅读和编写。同时也易于机器解析和生成。 它基于JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999的一个子集。 JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。 这些特性使JSON成为理想的数据交换语言。

优点:易于人的阅读和编写,易于程序的解析和生产
基本格式:{key:value}

标准的json数据表示
数据结构:Object、Array
基本类型:String、number、true、false、null

因此如果需要表示时间类型的时候需要使用约定格式的字符串如"YYYY-MM-DD"  
然后在后台对数据进行解析

数据结构——object
使用花括号{}包含的键值对结构,key必须是String类型,value为任何基本类型或数据结构
数据结构——Array
使用中括号[]来起始,并用逗号来分割元素

下面这个网站可以帮助你解析json字符串,并生成文件:
JSON Editor Online

{
  "array": [
    1,
    2,
    3
  ],
  "boolean": true,
  "null": null,
  "number": 123,
  "object": {
    "a": "b",
    "c": "d",
    "e": "f"
  },
  "string": "Hello World"
}
注意,JSON文件里不允许注释

具体资料可以查阅-->JSON中文官方网站
这里还有所有语言的示例代码以及相关工具等等内容,十分详尽,如

Java:
    JSON-java.
    JSONUtil.
    jsonp.
    Json-lib.
    Stringtree.
    SOJO.
    json-taglib.
    Flexjson.
    JON tools.
    Argo.
    jsonij.
    fastjson.
    mjson.
    jjson.
    json-simple.
    json-io.
    JsonMarshaller.
    google-gson.
    Json-smart.
    FOSS Nova JSON.
    Corn CONVERTER.
    Apache johnzon.
    Genson.
    JSONUtil.
    cookjson.

JavaScript:
    JSON.
    json2.js.
    clarinet.
    Oboe.js.

利用org.Json构建json数据

利用org.Json在Java中使用JSONObject对象

Json是Android SDK的官方库
适合移动端开发

例子 JSON和对象的转换,在pom.xml中添加依赖包

<!--JAVA ASON--> 
<dependency>  <groupId>org.json</groupId>
  <artifactId>json</artifactId>
  <version>20090211</version>
 </dependency>

测试类

    @Test
    public void testJSONObject(){
        BasicConfigurator.configure();
        JSONObject jsonObject = new JSONObject();
        Object nullObject= null;//使编译器跳过对null类型对检查
        try {
            jsonObject.put("name","张全蛋");
            jsonObject.put("age","25.2");
            jsonObject.put("birthday","1990-01-01");
            jsonObject.put("school","富土康");
            jsonObject.put("major", new String[]{"理发", "挖掘机"});
            jsonObject.put("hasGirlFriend",false);
            jsonObject.put("hascar",nullObject);
            jsonObject.put("comment","这是一个注释,因为json格式里是不能使用常规注释手段的");

            System.out.println(jsonObject.toString());
        } catch (JSONException e) {
            e.printStackTrace();
        }
        //output
        //{"birthday":"1990-01-01","major":["理发","挖掘机"],"school":"富土康","name":"张全蛋","comment":"这是一个注释,因为json格式里是不能使用常规注释手段的","hasGirlFriend":false,"age":"25.2"}
    }
}

输出

  {
    "birthday": "1990-01-01",
    "major": [
      "理发",
      "挖掘机"
    ],
    "school": "富土康",
    "name": "张全蛋",
    "comment": "这是一个注释,因为json格式里是不能使用常规注释手段的",
    "hasGirlFriend": false,
    "age": "25.2"
  }

Json在Java中使用字符串生成JSONObject对象

@Test
public void createJSONObjectByString(){
    JSONObject jsonObject = null;
    String jsonStr ="{\"errno\":0,\"data\":true,\"errmsg\":\"success\"}";
    try {
        jsonObject = new JSONObject(jsonStr);
    } catch (JSONException e) {
        e.printStackTrace();
    }
    if(jsonObject != null){
        System.out.println(jsonObject.toString());
        //out:
        // {"errno":0,"data":true,"errmsg":"success"}
    }
}

Json在Java中使用MAP生成JSONObject对象


    @Test
    public void createJSONObjectByMap() {
        Object nullObject = null;//使编译器跳过对null类型对检查

        Map<String, Object> jsonObject = new HashMap<String, Object>();
        jsonObject.put("name", "张全蛋");
        jsonObject.put("age", "25.2");
        jsonObject.put("birthday", "1990-01-01");
        jsonObject.put("school", "富土康");
        jsonObject.put("major", new String[]{"理发", "挖掘机"});
        jsonObject.put("hasGirlFriend", false);
        jsonObject.put("hascar", nullObject);
        jsonObject.put("comment", "这是一个注释,因为json格式里是不能使用常规注释手段的");

        System.out.println(new JSONObject(jsonObject).toString());

        //out:{"birthday":"1990-01-01","major":["理发","挖掘机"],"school":"富土康","name":"张全蛋","comment":"这是一个注释,因为json格式里是不能使用常规注释手段的","hasGirlFriend":false,"age":"25.2","hascar":null}
    }

Json在Java中使用JavaBean对象来生成JSONObject对象

    @Test
    public void createJSONObjectbyJavaBean() {
        //常用
        Message msg = new Message();
        msg.errno = 0;
        msg.errmsg = "success";
        msg.data = true;
        System.out.println(new JSONObject(msg).toString());

        //out:
        //{"errno":0,"data":true,"errmsg":"success"}
    }

从文件读取JSON数据

依赖

    <!--org.apache.commons.io.FileUtils-->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.4</version>
    </dependency>

测试文件的内容 与测试类文件同级 名为success_message.json

    {
        "errno": 0,
        "errmsg":["智商余额不足","需发红包给刘老师@95820608进行充值!"],
        "data":true
    }

测试类中的测试方法


    @Test
    public void readJSONFile() throws IOException, JSONException {
        //从当前类的同源文件中查找特定路径的文件
        File file = new File(ReadJSONFileSample.class.getResource("/success_message.json").getFile());
        String content = FileUtils.readFileToString(file);
        JSONObject jsonObject = new JSONObject(content);
        if(!jsonObject.isNull("errno")){
            System.out.println("错误代码 --> " + jsonObject.getInt("errno"));
        }
        if(!jsonObject.isNull("errmsg")){
            System.out.println("错误信息 --> " + jsonObject.getString("errmsg").toString());
        }
        if(!jsonObject.isNull("data")){
            System.out.println("错误内容 --> " + jsonObject.getBoolean("data"));
        }
        //out
        //错误代码 --> 0
        //错误信息 --> ["智商余额不足","需发红包给刘老师@95820608进行充值!"]
        //错误内容 --> true

        //将JSONObject中的属性值为数组的值读取出来遍历
        //如果值不是数组则会导致报错!
        if(!jsonObject.isNull("errmsg")){
            JSONArray jsonArray = jsonObject.getJSONArray("errmsg");
            for (int i = 0; i < jsonArray.length(); i++) {
                String m = (String)jsonArray.get(i);
                System.out.println(m);
            }
        }
        //out
        //智商余额不足
        //需发红包给刘老师@95820608进行充值!
    }
}

使用Gson 来更加灵活的构建json数据

gson是google提供的开源api
Gson功能更强大可以在json格式和javaObject之间通过反射灵活转换
更适合服务端后台的开发

  • 优点:
  • 1.轻量, 支持将json字符串反向生成指定类的对象
  • 2.支持日期格式

使用Gson生成json

pom.xml 添加依赖

<!--GOOGLE GSON-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.4</version>
</dependency>

测试类和测试方法


    //使用GSON注解更改序列化后JSON的属性名称
    protected class Message{
        @SerializedName("ERRNO")
        int errno;
        String errmsg;
        boolean data;
    }

    @Test
    public void gsonCreate(){
        Message msg = new Message();
        msg.errno = 0;
        msg.errmsg = "success";msg.data = true;

        Gson gson = new Gson();
        String jsonStr = gson.toJson(msg);
        System.out.println(jsonStr);
        //out:{"ERRNO":0,"errmsg":"success","data":true}
    }

使用gsonBuilder定制json的转换规则

    protected class FailMessage{
        private int errno;
        private String errmsg;
        private boolean data;
        private transient String  ignor;//transient 声明的属性不会被序列化
    }

    @Test
    public void gsonCreateByGsonBuilder(){
        FailMessage msg = new FailMessage();
        msg.errno = -1;
        msg.errmsg = "fail";msg.data = false;

        GsonBuilder gsonBuilder = new GsonBuilder();
        gsonBuilder.setPrettyPrinting();//美化格式后输出!方便调试
        gsonBuilder.setFieldNamingStrategy(new FieldNamingStrategy() {
        //通过回调函数修改反射得到的属性值
            @Override
            public String translateName(Field field) {
                if(field.getName().equals("errno")){
                    return ("error-code");
                }else{
                    return field.getName();
                }
            }
        });
        Gson gsonNew = gsonBuilder.create();
        System.out.println(gsonNew.toJson(msg));////美化格式后输出!方便调试
        //out:
        //        {
        //              "error-code": -1, //errno被修改为error-code
        //              "errmsg": "fail",
        //              "data": false
        //        }
    } 

使用gson反向将json字串转换成指定类

message.json文件内容如下:

    {
        "errno": 0,
        "errmsg":"智商余额不足,需发红包给刘老师@95820608进行充值!",
        "data":true
    }

测试类

public class Message {
  public int errno;
  public String errmsg;
  public boolean data;

    public int getErrno() {
        return errno;
    }

    public void setErrno(int errno) {
        this.errno = errno;
    }

    public String getErrmsg() {
        return errmsg;
    }

    public void setErrmsg(String errmsg) {
        this.errmsg = errmsg;
    }

    public boolean getData() {
        return data;
    }

    public void setData(boolean data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "Message{" +
                "errno=" + errno +
                ", errmsg='" + errmsg + '\'' +
                ", data=" + data +
                '}';
    }
}

测试方法

    @Test
    public void GsonReadTest() throws IOException {
        File file = new File(GsonSample.class.getResource("/message.json").getFile());
        String content = FileUtils.readFileToString(file);

        Gson gson = new Gson();
        //将json文件转换成指定类
        Message msg = gson.fromJson(content, com.niit.mvcdemo.model.Message.class);
        System.out.println(msg.toString());
        //out:
        //Message{errno=0, errmsg='智商余额不足,需发红包给刘老师@95820608进行充值!', data=true}
    }

反向将json字串转换成指定带有时间属性的类

    class Logger{
        private String content;
        private Date createtime;
        @Override
        public String toString() {
            return "Logger{content='" + content + '\'' + ", createtime=" + createtime.toString() +'}';
        }
    }

    @Test
    public void GsonFromJsonToObject() throws IOException {

        Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create();

        //使用gsonBuilder指定时间类型格式将json文件转换成指定类
        Logger logger = gson.fromJson("{\"content\":\"Application started...\",\"createtime\":\"1970-01-01 00:00:00\"}", json.GsonSample.Logger.class);
        System.out.println(logger.toLocalString());
        //out:Logger{content='Application started...', createtime=1970-1-1 0:00:00}
    }
小知识
yyyy-MM-dd HH:mm:ss
年-月-日 时:分:秒
大写是为了区分“月”与“分”
顺便说下HH为什么大写,是为了区分12小时制与24小时制。
小写的h是12小时制,大写的H是24小时制。
书写格式和语言规定有关,上述写法是Windows系统中的我们常见的写法,包括日期设置于办公软件在内。在其他语言中有类似的但使用符号或格式不同的写法。
有的时候我们会看到这样的格式:yyyy-M-d H:m:s
mm与m等,它们的区别为是否有前导零:H,m,s表示非零开始,HH,mm,ss表示从零开始。
比如凌晨1点2分,HH:mm显示为01:02,H:m显示为1:2。
以2014年1月1日凌晨1点1分1秒(当天是星期三)为例子介绍一下其他的:
yyyy/yyy/yy/y 显示为 2014/2014/14/4
        (3个y与4个y是一样的,为了便于理解多写成4个y)
MMMM/MMM/MM/M 显示为 一月/一月/01/1
        (4个M显示全称,3个M显示缩写,不过中文显示是一样的,英文就是January和J)
dddd/ddd/dd/d 显示为 星期三/周三(有的语言显示为“三”)/01/1
        (在英文中同M一样,4个d是全称,3个是简称;
dddd/ddd表示星期几,dd/d表示几号)
HH/H/hh/h 显示为 01/1/01 AM/1 AM
剩下的mm/m/ss/s只是前导零的问题了。
yyyy/M/d/dddd H:mm:ss 就是 2014年1月1日星期三 1:01:01

gson转换会自动将数组值映射成实体类对应的集合属性

注意Map会被映射成google自定义的Map类型..

class Product{
    private String title;
    private String[] array;
    private List list;
    private Set set;
    private Map map;
}

@Test
public void GsonMappingForCollection(){
    String jsonStr= "" +
            "{\n" +
            "  \"title\": \"product_id_1\",\n" +
            "  \"array\": [\n" +
            "    \"array1\",\n" +
            "    \"array2\"\n" +
            "  ],\n" +
            "  \"list\": [\n" +
            "    \"list1\",\n" +
            "    \"list2\"\n" +
            "  ],\n" +
            "  \"set\": [\n" +
            "    \"set2\",\n" +
            "    \"set1\"\n" +
            "  ],\n" +
            "  \"map\": {\n" +
            "    \"1\": \"map1\",\n" +
            "    \"2\": \"map2\"\n" +
            "  }\n" +
            "}"
            ;
    //通过gson将制定类的实例转化成json字符串
    Product product = new Gson().fromJson(jsonStr,GsonSample.Product.class);
    System.out.println(product.title.toString() + "\t\t" + product.title.getClass());
    //out:        product_id_1          class java.lang.String
    System.out.println(Arrays.toString(product.array) + "\t\t" + product.array.getClass());
    //out:        [array1, array2]      class [Ljava.lang.String;
    System.out.println(product.set.toString() + "\t\t" + product.set.getClass());
    //out:        [set2, set1]          class java.util.LinkedHashSet
    System.out.println(product.list.toString() + "\t\t" + product.list.getClass());
    //out:        [list1, list2]        class java.util.ArrayList
    System.out.println(product.map.toString() + "\t\t" + product.map.getClass());
    //out:        {1=map1, 2=map2}      class com.google.gson.internal.LinkedTreeMap
}

jackson实现Json序列化和反序列化

利用fasterxml.jackson实现JSON序列化和反序列化

Gradle依赖
fasterxml.jackson依赖jackson-core, jackson-databind和jackson-annotations.
示例如下:

compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.5'
compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.9.5'
compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.5'

序列化和反序列化实现

JSON工具类
我们构造一个JSON工具类,实现JSON序列化和反序列化, 主要用到的类为com.fasterxml.jackson.databind.ObjectMapper, 代码示例如下:

package com.notepad.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.HashMap;
import java.util.Map;

public class JsonSerializer {

    /**
     * JSON序列化
     *
     * @param object 对象
     * @return JSON字符串
     */
    public static String serialize(Object object) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writeValueAsString(object);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return "";
        }
    }

    /**
     * JSON字符串反序列化
     *
     * @param jsonStr JSON字符串
     * @return a Map
     */
    public static Map deserialize(String jsonStr) {
        try {
            return deserialize(jsonStr, Map.class);
        } catch (Exception e) {
            e.printStackTrace();
            return new HashMap();
        }
    }

    public static <T> T deserialize(String jsonStr, Class<T> classType) throws Exception {
        return new ObjectMapper().readValue(jsonStr, classType);
    }
}

Entity
为测试我们编写的JSON工具类, 定义一个Entity对象。
为了构建的Entity对象被JSON工具类操作, 关于Entity有几点说明:

  • Entity对象必须有默认构造函数
  • 成员变量必须有对应的Setter方法
  • 可选: 可通过@JsonProperty自定义序列化和反序列化对应的字符的名称,如@JsonProperty(“UID”),则序列化时uid字段显示为UID,同理反序列化时找到字符串中UID对应的值,复制给uid。
  • 可选:可通过@JsonIgnore注解,过滤掉不需要进行序列化的成员变量。

示例如下:

package com.notepad.thinkingnote.domain;

import com.fasterxml.jackson.annotation.JsonProperty;

public class Entity {

    public Entity() {}

    public Entity(String uid, String name) {
        this.uid = uid;
        this.name = name;
    }

    /** 实体标识符 */
    @JsonProperty("UID")
    private String uid;

    @JsonProperty("name")
    private String name;

    public void setUid(String uid) {
        this.uid = uid;
    }

    public String getUid() {
        return uid;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

单元测试

编写单元测试,测试Entity的序列化和反序列化, 示例如下:

package com.notepad.thinkingnote.domain;

import com.notepad.util.JsonSerializer;
import org.junit.Test;

import static org.junit.Assert.*;

public class EntityTest {

    @Test
    public void testSerialize() throws Exception {
        Entity entity = new Entity("James", "James");
        System.out.println(JsonSerializer.serialize(entity));
    }

    @Test
    public void testDeserialize() throws Exception {
        String test = "{\"UID\":\"James\",\"name\":\"James\"}";
        Entity entity = JsonSerializer.deserialize(test, Entity.class);
        System.out.println(entity.getUid() + ":" + entity.getName());
    }
}

输出结果如下:
entity的序列化结果, 显示字段UID
{"UID":"James","name":"James"}

entity的序列化结果
James:James

UnrecognizedPropertyException异常解决

通过上面的介绍, 我们基本了解了JSON的序列化和反序列化,不过有时我们会遇到一种问题。
考虑这样一种情况,我们针对Http接口返回的JSON字符串,构建了一个具体的Entity对象,但是当Http接口中突然增加了一个字段type,如果还是按照原来方式解析会如何呢?

@Test
    public void testDeserialize() throws Exception {
        String test = "{\"UID\":\"James\",\"name\":\"James\", \"type\":\"entity\"}";
        Entity entity = JsonSerializer.deserialize(test, Entity.class);
        System.out.println(entity.getUid() + ":" + entity.getName());
    }

出现UnrecognizedPropertyException异常:

com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "type" ....

即对于新添加的type字符无法识别。如何解决这个问题呢?我们这里提供2种方法。

  • 方法1 JsonIgnoreProperties注解
    利用fasterxml.jackson提供的@JsonIgnoreProperties注解,针对无法识别的属性进行过滤。这里主要是修改需要进行反序列化的对象Entity,示例如下:
    针对无法识别的属性进行过滤

    @JsonIgnoreProperties(ignoreUnknown = true)
    public class Entity {...}
  • 方法2 DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
    方法1的修改需要对每一个需要进行反序列化的类进行修改, 不太方便。
    方法2通过修改JSON工具类的反序列化方法,设置DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES的值为false, 可以仅一次修改就适用全部对象。
    示例如下:

    public static <T> T deserialize(String jsonStr, Class<T> classType) throws Exception {
        // 添加configure, 设置DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES为false
        // 则对于无法识别的属性直接过滤
        return new ObjectMapper()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .readValue(jsonStr, classType);
    }

Views: 35

Java多线程及Future用法

Java多线程及Future用法

同步和异步 - 比要发射10枚导弹,同步的方式就是上一枚导弹炸毁后才发射下一枚,而异步就是全部挨个发射出去,而不在乎它们是否击中目标,这种异步方式也被称为Fire and Forget。Kafka为了提高吞吐性能默认是异步发送消息的。为了更好的了解Kafka的Producer发送,我们先补充一些关于Java多线程的知识。

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。

  • 继承Thread创建线程(不推荐)
  • 实现Runnable接口创建线程
  • 实现Callable接口实现线程
  • 使用线程池Executor创建线程(推荐)

1.继承Thread实现线程

  我们先来看一下Thread的源码,它是一个类,同样也实现了Runnable接口

public Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }

    private volatile String name;
    private int            priority;
    private Thread         threadQ;
    private long           eetop;

    /* Whether or not to single_step this thread. */
    private boolean     single_step;

    /* Whether or not the thread is a daemon thread. */
    private boolean     daemon = false;

    /* JVM state */
    private boolean     stillborn = false;

    /* What will be run. */
    private Runnable target;

    /* The group of this thread */
    private ThreadGroup group;

    /* The context ClassLoader for this thread */
    private ClassLoader contextClassLoader;

    /* The inherited AccessControlContext of this thread */
    private AccessControlContext inheritedAccessControlContext;

    /* For autonumbering anonymous threads. */
    private static int threadInitNumber;
    private static synchronized int nextThreadNum() {
        return threadInitNumber++;
    }

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

    /*
     * The requested stack size for this thread, or 0 if the creator did
     * not specify a stack size.  It is up to the VM to do whatever it
     * likes with this number; some VMs will ignore it.
     */
    private long stackSize;

    /*
     * JVM-private state that persists after native thread termination.
     */
    private long nativeParkEventPointer;

    /*
     * Thread ID
     */
    private long tid;

    /* For generating thread ID */
    private static long threadSeqNumber;

    /* Java thread status for tools,
     * initialized to indicate thread 'not yet started'
     */

    private volatile int threadStatus = 0;

    //......

}

通过继承Thread类来创建并启动多线程的一般步骤如下

  1. 定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
  2. 创建Thread子类的实例,也就是创建了线程对象
  3. 启动线程,即调用线程的start()方法

代码示例:

public class  ThreadTest {

    public static void main(String[] args) {
        new MyThread().start();
    }

   static class MyThread extends Thread {//继承Thread
        public void run() {
            System.out.println("我是继承Thread类!! ");
        }
    }

}

2.实现Runnable接口创建线程

  我们来看一下Runnable的源码,它是一个接口:

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

由于run()方法返回值为void类型,所以在执行完任务之后无法返回任何结果。

通过实现Runnable接口创建并启动线程一般步骤如下:

  1. 定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体
  2. 创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象
  3. 第三部依然是通过调用线程对象的start()方法来启动线程

代码示例:

public class RunnableTest {

    public static void main(String[] args) {
        MyThread2 myThread=new MyThread2();
        Thread thread = new Thread(myThread);
        thread.start();
    }

    static class MyThread2 implements Runnable {
        @Override
        public void run() {
            System.out.println("我是实现Runnable接口!! ");
        }
    }
}

3.实现callable接口实现线程

  我们来看一下callable源码,它是一个接口:

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

它和Runnable接口不一样的是,call()方法提供了2个额外功能:

  • call()方法可以有返回值
  • call()方法可以声明抛出异常

java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。

那么怎么使用Callable呢?一般情况下是配合ExecutorService来使用的,在ExecutorService接口中声明了若干个submit方法的重载版本:

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

第一个submit方法里面的参数类型就是Callable。

暂时只需要知道Callable一般是和ExecutorService配合来使用的,具体的使用方法讲在后面讲述。

一般情况下我们使用第一个submit方法和第三个submit方法,第二个submit方法很少使用。

3.1 Future

我们来看一下Future的源码,它是一个接口,用来返回子线程的计算结果:

public interface Future<V> {

    /**
     * Attempts to cancel execution of this task.  This attempt will
     * fail if the task has already completed, has already been cancelled,
     * or could not be cancelled for some other reason. If successful,
     * and this task has not started when {@code cancel} is called,
     * this task should never run.  If the task has already started,
     * then the {@code mayInterruptIfRunning} parameter determines
     * whether the thread executing this task should be interrupted in
     * an attempt to stop the task.
     *
     * <p>After this method returns, subsequent calls to {@link #isDone} will
     * always return {@code true}.  Subsequent calls to {@link #isCancelled}
     * will always return {@code true} if this method returned {@code true}.
     *
     * @param mayInterruptIfRunning {@code true} if the thread executing this
     * task should be interrupted; otherwise, in-progress tasks are allowed
     * to complete
     * @return {@code false} if the task could not be cancelled,
     * typically because it has already completed normally;
     * {@code true} otherwise
     */
    boolean cancel(boolean mayInterruptIfRunning);

    /**
     * Returns {@code true} if this task was cancelled before it completed
     * normally.
     *
     * @return {@code true} if this task was cancelled before it completed
     */
    boolean isCancelled();

    /**
     * Returns {@code true} if this task completed.
     *
     * Completion may be due to normal termination, an exception, or
     * cancellation -- in all of these cases, this method will return
     * {@code true}.
     *
     * @return {@code true} if this task completed
     */
    boolean isDone();

    /**
     * Waits if necessary for the computation to complete, and then
     * retrieves its result.
     *
     * @return the computed result
     * @throws CancellationException if the computation was cancelled
     * @throws ExecutionException if the computation threw an
     * exception
     * @throws InterruptedException if the current thread was interrupted
     * while waiting
     */
    V get() throws InterruptedException, ExecutionException;

    /**
     * Waits if necessary for at most the given time for the computation
     * to complete, and then retrieves its result, if available.
     *
     * @param timeout the maximum time to wait
     * @param unit the time unit of the timeout argument
     * @return the computed result
     * @throws CancellationException if the computation was cancelled
     * @throws ExecutionException if the computation threw an
     * exception
     * @throws InterruptedException if the current thread was interrupted
     * while waiting
     * @throws TimeoutException if the wait timed out
     */
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

我们来看一下它的各个方法:

  • boolean cancel(boolean mayInterruptIfRunning):用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
  • boolean isCancelled():如果在Callable任务正常完成前被取消,返回True
  • boolean isDone():若Callable任务完成,返回True
  • V get() throws InterruptedException, ExecutionException:返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值
  • V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException:用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null

因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask

3.2 FutureTask

我们先来看一下FutureTask的实现:

public class FutureTask<V> implements RunnableFuture<V> {}

FutureTask类实现了RunnableFuture接口,我们看一下RunnableFuture接口的实现:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

FutureTask提供了2个构造器:

public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
}
public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
}

事实上,FutureTask是Future接口的一个唯一实现类。

3.3 使用FutureTask对象作为Thread对象的target创建并启动线程

接下来我们看如何创建并启动有返回值的线程:

  1. 创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
  2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
  3. 使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

代码示例:

public class CallableAndFuture {
    public static void main(String[] args) {

        Callable<Integer> call = new Callable<Integer>() {
            public Integer call() throws Exception {
                System.out.println("计算线程正在计算结果...");
                Thread.sleep(3000);
                return 1;
            }
        };
        FutureTask<Integer> future = new FutureTask<>(call);

        new Thread(future,"有返回值的线程").start();//实质上还是以Callable对象来创建并启动线程

        try {
            System.out.println("子线程的返回值:" + future.get());//get()方法会阻塞,直到子线程执行结束才返回
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

3.4 使用executor创建线程

3.4.1.使用Callable+Future获取执行结果

代码示例:

public class CallableAndFuture {

    public static void main(String[] args) {
        /** Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。   */
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        Future<Integer> result = executor.submit(task);

        executor.shutdown();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        System.out.println("主线程在执行任务");

        try {
            System.out.println("task运行结果"+result.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        System.out.println("所有任务执行完毕");
    }

    static class Task implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("子线程在进行计算");
            Thread.sleep(3000);
            int sum = 0;
            for(int i=0;i<100;i++)
                sum += i;
            return sum;
        }
    }
}
3.4.2.使用Callable+FutureTask获取执行结果
public class CallableAndFuture2 {
    public static void main(String[] args) {
        //第一种方式
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        executor.submit(futureTask);
        executor.shutdown();

        //第二种方式,注意这种方式和第一种方式效果是类似的,只不过一个使用的是ExecutorService,一个使用的是Thread
        /*Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        Thread thread = new Thread(futureTask);
        thread.start();*/

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        System.out.println("主线程在执行任务");

        try {
            System.out.println("task运行结果"+futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        System.out.println("所有任务执行完毕");
    }

    static class Task implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("子线程在进行计算");
            Thread.sleep(3000);
            int sum = 0;
            for(int i=0;i<100;i++)
                sum += i;
            return sum;
        }
    }
}

4.使用线程池Executor创建线程

4.1 Executor执行Runnable

public class ExecutorRunnable {

    /**
     * 从结果中可以看出,pool-1-thread-1和pool-1-thread-2均被调用了两次,这是随机的,execute会首先在线程池中选择
     * 一个已有空闲线程来执行任务,如果线程池中没有空闲线程,它便会创建一个新的线程来执行任务。
     */
    public static void main(String[] args){
        ExecutorService executorService = Executors.newCachedThreadPool();
//      ExecutorService executorService = Executors.newFixedThreadPool(5);
//      ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 5; i++){
            executorService.execute(new TestRunnable());
            System.out.println("************* a" + i + " *************");
        }
        executorService.shutdown();
    }
}

     class TestRunnable implements Runnable {
        public void run() {
            System.out.println(Thread.currentThread().getName() + "线程被调用了。");
        }

}

执行结果:

************* a1 *************
************* a2 *************
pool-1-thread-2线程被调用了。
************* a3 *************
pool-1-thread-1线程被调用了。
pool-1-thread-2线程被调用了。
************* a4 *************
pool-1-thread-3线程被调用了。

复制代码

4.2Executor执行Callable

public class Executor执行Callable {
    /**
     *  从结果中可以同样可以看出,submit也是首先选择空闲线程来执行任务,如果没有,才会创建新的线程来执行任务。
     *  另外,需要注意:如果Future的返回尚未完成,则get()方法会阻塞等待,直到Future完成返回,可以通过
     *  调用isDone()方法判断Future是否完成了返回。
     */
    public static void main(String[] args){
        ExecutorService executorService = Executors.newCachedThreadPool();
        List<Future<String>> resultList = new ArrayList<Future<String>>();

        //创建10个任务并执行
        for (int i = 0; i < 10; i++){
            //使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中
            Future<String> future = executorService.submit(new TaskWithResult(i));
            //将任务执行结果存储到List中
            resultList.add(future);
        }

        //遍历任务的结果
        for (Future<String> fs : resultList){
            try{
                while(!fs.isDone());//Future返回如果没有完成,则一直循环等待,直到Future返回完成
                System.out.println(fs.get());     //打印各个线程(任务)执行的结果
            }catch(InterruptedException e){
                e.printStackTrace();
            }catch(ExecutionException e){
                e.printStackTrace();
            }finally{
                //启动一次顺序关闭,执行以前提交的任务,但不接受新任务
                executorService.shutdown();
            }
        }
    }
}

class TaskWithResult implements Callable<String> {
    private int id;

    public TaskWithResult(int id){
        this.id = id;
    }

    /**
     * 任务的具体过程,一旦任务传给ExecutorService的submit方法,
     * 则该方法自动在一个线程上执行
     */
    public String call() throws Exception {
        System.out.println("call()方法被自动调用!!!    " + Thread.currentThread().getName());
        //该返回结果将被Future的get方法得到
        return "call()方法被自动调用,任务返回的结果是:" + id + "    " + Thread.currentThread().getName();
    }
}

执行结果:

call()方法被自动调用!!!    pool-1-thread-1
call()方法被自动调用,任务返回的结果是:0    pool-1-thread-1
call()方法被自动调用!!!    pool-1-thread-2
call()方法被自动调用,任务返回的结果是:1    pool-1-thread-2
call()方法被自动调用!!!    pool-1-thread-3
call()方法被自动调用,任务返回的结果是:2    pool-1-thread-3
call()方法被自动调用!!!    pool-1-thread-5
call()方法被自动调用!!!    pool-1-thread-6
call()方法被自动调用!!!    pool-1-thread-7
call()方法被自动调用!!!    pool-1-thread-9
call()方法被自动调用!!!    pool-1-thread-4
call()方法被自动调用,任务返回的结果是:3    pool-1-thread-4
call()方法被自动调用,任务返回的结果是:4    pool-1-thread-5
call()方法被自动调用,任务返回的结果是:5    pool-1-thread-6
call()方法被自动调用,任务返回的结果是:6    pool-1-thread-7
call()方法被自动调用!!!    pool-1-thread-8
call()方法被自动调用,任务返回的结果是:7    pool-1-thread-8
call()方法被自动调用,任务返回的结果是:8    pool-1-thread-9
call()方法被自动调用!!!    pool-1-thread-10
call()方法被自动调用,任务返回的结果是:9    pool-1-thread-10

Views: 61

Java集合框架

集合框架是 Java 中最重要的内容之一。无论是最基本的 Java SE 应用程序开发,还是企业级的 Java EE 程序开发,集合都是开发过程中常用的部分。

1 集合的基本概念

首先,什么是集合呢?
集合是一种对象,只不过这种对象的功能,是储存和管理多个对象。例如,我们生活中的“抽屉”对象,抽屉就是用来放东西的,也就是说,“抽屉”这个对象的功能,就是用来储存和管理多个对象的。
那是不是除了集合之外,就没有别的管理多个对象的方式了呢?不是。我们之前学到的
数组,就能够完成储存和管理多个对象的功能。

那使用数组管理和储存多个对象,有什么问题呢? 看下面这个需求:

  1. 创建一个长度为 3 的字符串数组,在数组中放入“zhang3”,“li4”,“wang5”这三个字符串。

  2. 在下标为 1 的位置插入“zhao6”字符串(这意味着需要进行数组扩容)

  3. 删除“li4”这个字符串。

上面这段需求,用数组怎么实现呢?

参考实现代码如下:

public class TestArray {
    public static void main(String args[]){ 
        String[] names = new String[3]; names[0] = "zhang3";
        names[1] = "li4"; names[2] = "wang5";

        //插入 zhao6 之前需要扩容
        String[] newNames = new String[names.length * 2]; for(int i = 0; i<names.length; i++){
        newNames[i] = names[i];
        }
        names = newNames;

        //插入(不使用 for 循环,而直接赋值)
        names[3] = names[2]; names[2] = names[1]; names[1] = "zhao6";

        //删除(不使用 for 循环)
        names[2] = names[3];
    }
}

可以看出,用数组也可以实现相应的扩容、插入、删除等操作。但是,是用数组进行这些相关操作,却非常的不方便,需要撰写大量的基础代码。这些代码繁琐、重复,而且容易出错(很有可能产生数组下标越界异常等),有没有办法把程序员从这种繁重的劳动中解放出来呢?

我们可以把数组、以及对数组相关的操作封装在一个类中。例如,我们封装一个 MyList类:

public class MyList {
    //data 用来保存数组数据
    private Object[] data;
    //index 保存有效元素的个数
    private int index;
    //初始数组长度是 5,有效元素个数为 0 
    public MyList(){
        data = new Object[5]; index = 0;
    }
    //把 value 元素放到末尾 如果 data 数组已满则自动扩充
    public void add(Object value){
    …
    }
    //把 value 元素插入在 pos 位置 如果 data 数组已满则自动扩充
    public void add(int pos, Object value){
    …
    }
    //删除 pos 位置的元素
    public void delete(int pos){
    …
    }

    //获得下标为 pos 的元素
    public Object get(int pos){
    …

    }
    //获得有效元素的个数public int size(){
    …
    }
    //判断数组中是否包含 obj 对象
    //如果存在则返回 true
    //否则返回 false
    public boolean contains(Object obj){
    …
    }
}

注意几个要点。

  1. 为了能够让 MyList 类更通用,能够保存 Java 中任何一种对象,我们把数据类型设为 Object 类型。由于 Object 类型是Java 中所有类型的父类,因此可以把任何对象都赋值给 Object 类型的引用,也就是说,能够把任何一种对象都放入 Object 数组。如果遇到基本类型的数据,也能将数据转换为包装类对象,同样可以放入 Object 数组。
  2. MyList 类有一个属性 index,这个属性用来保存有效元素的个数。例如,刚开始的时候,创建了一个长度为 5 的 Object 类型数组,但是这相当于有 5 个元素的空间。但是刚创建的时候,数组中并没有存入有价值的数据,因此有效元素的个数为 0 个,index 值也为0。再举一个例子,假设在一个长度为 10 的数组中,存入了 10 个元素。之后,调用了一次delete 方法,此时,由于删掉了一个元素,因此有效元素的个数变为 9 个。虽然有效元素的个数变少了,但是数组的长度并没有减小,数组的长度依然是 10。利用 index 属性,就可以判断数组是不是已经满了(如果 index 等于 data.length,则意味着数组满了)。在执行插入操作时,如果发现数组已满,则自动完成数组长度的扩充。
  3. MyList 中,封装了数据 data,并且封装了跟数据相关的操作,例如对数组进行增加、删除和插入等操作。这样,我们就把数组这种数据,和对数组的基本操作封装在了一起,从而再遇到数组的一些插入和删除操作时,可以不用重新实现复杂的数组插入和删除操作,而直接利用 MyList 类中封装的函数。这样,通过封装 MyList 类,减少了程序员的工作量,也提高了代码的重用性。

例如,利用 MyList 改写之前那个数组的练习,代码下可以修改如下:

public class TestMyList {
    public static void main(String args[]){ MyList list = new MyList();
        //初始化
        list.add("zhang3");
        list.add("li4");
        list.add("wang5");

        // 插 入 zhao6 list.add(1, "zhao6");

        //删除 li4

        list.delete(2);

    }
}

可以看到,利用了 list 的 add 和 delete 方法,可以让程序员省略自己实现数组插入删除的麻烦。也就是说,MyList 封装了数组的插入和删除操作,让程序员可以直接调用而不用自己重新实现。
完整的 MyList 代码如下:

public class MyList {
    //data 用来保存数组数据
    private Object[] data;
    //index 保存有效元素的个数
    private int index;

    //初始数组长度是 5,有效元素个数为 0 public MyList(){
    data = new Object[5]; index = 0;
    }

    //把 value 元素放到末尾
    public void add(Object value){
        if(index == data.length) this.expand(); data[index] = value;
        index++;
    }

    //把 value 元素插入在 index 位置
    public void add(int pos, Object value){ if(index == data.length) this.expand(); for(int i = index; i>pos; i--){
        data[i] = data[i-1];
        }
        data[pos] = value; index++;
        }

        //删除 index 位置的元素
        public void delete(int pos){ for(int i=pos;i<index-1;i++){
        data[i] = data[i+1];
        }
        index--;
        }

        //获得有效元素的个数public int size(){
        return index;
    }

    //判断数组中是否包含 obj 对象
    //如果存在则返回 true
    //否则返回 false
    public boolean contains(Object obj){ for(int i = 0; i<index; i++){
        if (data[i].equals(obj)) return true;
        }
        return false;
        }
        //获得下标为 pos 的元素
        public Object get(int pos){ return data[pos];
    }

    private void expand(){
        Object[] newArray = new Object[data.length * 2]; for(int i = 0; i<data.length; i++){
        newArray[i] = data[i];
        }
        data = newArray;
    }
}

2 集合框架概览

类似于 MyList 这样的类,其实,Sun 公司已经为我们写好了,完全不需要我们自己实现。这就是 Sun 公司提供的集合框架。对于我们来说,重要的不是如何实现这些类,而是如何使用 Sun 公司提供给我们的集合类。
集合就是 Sun 公司为程序员写的很多类。这些类用来储存和管理多个对象。当然,对于管理多个对象来说,管理的方式和特点多种多样。对不同的管理方式会有不同的类来实现。将这些集合类提炼出共性,就能够提炼出很多不同的接口。这些包含着共性的接口,就是我们学习集合重点要掌握的内容。
下面就是 Java 集合框架中,几个主要的接口:

file

上图是 Java 集合框架中主要的接口。我们分别对每个接口进行描述。

  1. Collection 接口。这个接口的特点是:元素是 Object。换而言之,Collection 接口所有的子接口,以及其实现类,所管理的元素单位都是对象。
  2. Map 接口。与 Collection 接口对应,Map 接口所管理的元素不是对象,而是“键值对”。什么是键值对呢?“键”和“值”各是一个对象,这两个对象之间,存在着对应的关系,我们可以通过键对象,来找到对应的值对象。在 Map 中,键对象是唯一的,不可重复的,而键对象所对应的值对象是可以重复的。
    例如,每隔四年,都会举办一次世界杯,经过艰苦的捉对厮杀,最终会有一个世界杯冠军产生。这样,举办世界杯的年份,和世界杯冠军,组成了一个对应的关系。上面所说的对应的关系,就是“键值对”的关系。值可以重复,但是键却是唯一的。例如,世界杯举办年份和世界杯冠军的对应关系中,键是世界杯举办的年份,而值是世界杯冠军的获得者。世界杯举办年份这个键不可能重复。例如,2002 年世界杯冠军为巴西队,则“2002—巴西”形成一个键值对的关系。2002 这个键不能够重复, 因为 2002 年只有一个世界杯冠军。而巴西队在 1994 年也获得过世界杯冠军,因此
    “1994—巴西”也形成一个键值对。由此可见,值对象可以重复。
  3. Collection 有两个子接口,其中一个子接口为 List 接口。List 接口的特点,是 List 中元素有顺序,可以重复。所谓元素有顺序,指的是说,几个元素放入 List 的先后顺序,就是这几个元素在 List 中的排列顺序。通过集合中元素的顺序,我们可以区分出集合中第 1 个元素,第 2 个元素……
  4. Collection 还有一个子接口 Set 接口。Set 接口的特点是元素不可以重复,无顺序。例如,在一家饭店中,有“蒸羊羔”、“蒸熊掌”、“蒸鹿尾”三道菜。对于厨师来说, 他会做这三道菜,可以认为他会做的菜放在一个 Set 集合中,顾客可以从这个集合中挑选若干道菜。在这个集合中,没有元素重复(不会有个厨师跟顾客说,我会做蒸羊羔,还有蒸熊掌,还有蒸羊羔、还有蒸熊掌„„),并且元素的顺序也不重要,没有第 1 个第 2 个之分。
  5. Set 接口有个子接口 SortedSet。这个接口具有 Set 的特点,其中的元素不能够重复。
    但是这个接口与 SortedSet 不同的地方在于,这个接口中的元素会按照一定的排序规则,自动对集合中的元素排序。
  6. Map 有个子接口 SortedMap。这个接口与 Map 一样,管理的元素是键值对,键不能重复,值可以重复。所不同的是,在这个接口中,键对象会按照一定的排序规则, 自动排序。

下面我们就针对这几种接口,分别进行学习和讨论。
要掌握每种集合接口,就要重点掌握集合接口的这几个方面:
1、 接口的特点
2、 接口中定义的基本操作
3、 该集合如何遍历
4、 接口的不同实现类,以及实现类之间的区别

3 Collection

1、 接口特点
Collection 接口的特点是元素是 Object。遇到基本类型数据,需要转换为包装类对象。
2 、基本操作
Collection 接口中常用的基本操作罗列如下:

  • boolean add(Object o)
    这个操作表示把元素加入到集合中。add 方法的返回值为 boolean 类型。如果元素加入集合成功,则返回 true,否则返回 false。
  • boolean contains(Object o)
    这个方法判断集合中是否包含了 o 元素。
  • boolean isEmpty()
    这个方法判断集合是否为空。
  • Iterator iterator()
    这个方法很重要,可以用来完成集合的迭代遍历操作。
  • boolean remove(Object o)
    remove 方法表示从集合中删除 o 元素。返回值表示删除是否成功。
  • void clear()
    clear 方法清空集合。
  • int size()
    获得集合中元素的个数。

3、 Collection 如何遍历Collection 的实现类
Collection 没有直接的实现类。也就是说,某些实现类实现了 Collection 接口的子接口, 例如 List、Set,这样能够间接的实现 Collection 接口。但是没有一个实现类直接实现了Collection 接口却没有实现其子接口。
正因为如此,Collection 如何遍历,我们会在讲解其子接口时详细阐述。

4 List

4.1 List 特点和基本操作

List 接口的特点:元素是对象,并且元素有顺序,可以重复。
可以把 List 当做是一个列表。例如,如果让我们列出历任美国总统,我们必然会按照顺序说出每一任的人名,对于连任的总统,在这个列表中就会出现多次。这样的结构就是一个典型的 List:元素有顺序,并且可以重复出现。
对于 List 而言,元素的所谓“顺序”,指的是每个元素都有下标。因此,List 的基本操作,除了从 Collection 接口中继承来的之外,还有很多跟下标相关的操作。基本操作罗列如下:

  • boolean add(Object o) / void add(int index, Object element)
    在 List 接口中有两个重载的 add 方法。第一个 add 方法是从 Collection 接口中继承而来的,表示的是把 o 元素加入到 List 的末尾;第二个 add 方法是 List 接口特有的方法,表示的是把元素 element 插入到集合中 index 下标处。
  • Object get(int index) / Object set(int index, Object element)
    get 方法获得 List 中下标为 index 的元素,set 方法把 List 中下标为 index 的元素设置为 element。
    利用这两个方法,可以对 List 根据相应下标进行读写。
  • int indexOf(Object o)
    这个方法表示在 List 中进行查找。如果 List 中存在 o 元素,则返回相应的下标。如果 List 中不存在 o 元素,则返回-1。
    这个方法可以用来查找某些元素的下标。

除此之外,List 接口中还有一些诸如 size、clear、isEmpty 等方法,这些方法与介绍 Collection 接口中的相应方法含义相同,在此不再赘述。

4.2 遍历

首先,为了能使用 List 接口,必须先简单介绍一个 List 接口的实现类:ArrayList。这个类使用数组作为底层数据结构,实现了 List 接口,我们使用这个类来演示应该如何对 List 进行遍历。
由于 List 接口具有下标,因此我们用类似对数组遍历的方式,采用 for 循环对 List 进行遍历。示例代码如下:

public class TestArrayList {
public static void main(String args[]){ 
        List list = new ArrayList(); list.add("hello");
        list.add("world");
        list.add("java");
        list.add("study");

        for(int i = 0; i<list.size(); i++){ System.out.println(list.get(i));
        }

    }
}

可以看出,利用 List 接口中的 size 方法,我们可以得出集合中元素的个数,那么集合中元素的下标范围就是 0 ~ size-1 。继而我们可以通过 get 方法,根据下标获得相应的元素。利用这种方法,我们就可以遍历一个 List。
除此之外,List 接口还有另外一种遍历方式:迭代遍历。

4.2.1 迭代遍历
迭代遍历是 Java 集合中的一个比较有特色的遍历方式。这种方法被用来遍历 Collection 接口,也就是说,既可以用来遍历 List,也可以用来遍历 Set。
使用迭代遍历时,需要调用集合的 iterator 方法。这个方法在 Collection 接口中定义, 也就是说,List 接口也具有 iterator 方法。
这个方法的返回值类型是一个 Iterator 类型的对象。Iterator 是一个接口类型,接口类型没有对象,由此可知,调用 iterator 方法,返回的一定是 Iterator 接口的某个实现类的对象。这是非常典型的把多态、接口用在方法的返回值上面。
Iterator 接口表示的是“迭代器”类型。利用迭代器,我们可以对集合进行遍历,这种遍历方式即称之为迭代遍历。
例如,有如下代码:

public class TestArrayList {
    public static void main(String args[]){ 
        List list = new ArrayList(); list.add("hello");
        list.add("world");
        list.add("java");
        list.add("study");

        Iterator iter = list.iterator();
    }
}

此时,在集合中有四个元素:hello、world、java、study。

在调用 list 的 iterator 方法之后,返回一个 Iterator 类型的对象。这个对象就好像一个指针,指向第一个元素之前。如下图:
file

在迭代器中定义了两个方法:

  • boolean hasNext()
    这个方法返回一个 boolean 类型,表示判断迭代器右方还有没有元素。对于上面这种情况,对迭代器调用 hasNext()方法,返回值为 true。
  • Object next()
    这个方法,会把迭代器向右移动一格。同时,这个方法返回一个对象,这个对象就是迭代器向右移动时,跳过的那个对象。如下图:
    file
    调用一次 next 方法之后,迭代器向右移动一位。在向右移动的同时,跳过了“hello” 这个元素,于是这个元素就作为返回值返回。

我们可以再持续的调用 next()方法,依次返回“world”,“java”,“study”对象,直至 hasNext()方法返回 false,意味着迭代器已经指向了集合的末尾,遍历过程即结束。

利用 Iterator 接口以及 List 中的 iterator 方法,可以对整个 List 进行遍历。代码如下:

public class TestArrayList {
    public static void main(String args[]){ 
        List list = new ArrayList(); list.add("hello");
        list.add("world");
        list.add("java");
        list.add("study");
        Iterator iter = list.iterator(); while(iter.hasNext()){
        Object value = iter.next(); System.out.println(value);
    }

    }
}

迭代遍历往往是用 while 循环来实现的。利用 hasNext 方法返回值作为循环条件,判断List 后面是否还有其他元素。在循环体中,调用 next 方法,一方面把迭代器向后移动,另外一方面一一返回 List 中元素的值。

4.3 实现类

List 接口有以下几个实现类。要注意的是,这几个类都实现了 List 接口,也就是说,如果我们针对 List 接口编程的话,使用不同的实现类,编程的方式是一样的。例如,ArrayList 和 LinkedList 都实现了 List 接口,如果我们要把上一小节的程序里的实现由 ArrayList 替换成 LinkedList,只需要把这一句代码

List list = new ArrayList();

改为

List list = new LinkedList();

其余代码由于都是针对 List 接口的,因此完全不需要修改。也就是说,不管实现类是什么样子,对 List 接口的操作是一样的。这也从一个侧面反映了接口的作用:解耦合。

下面针对不同的实现类,分别进行一下介绍。

4.3.1 ArrayList 和 LinkedList

ArrayList 的特点是采用数组实现。很类似于之前我们写的 MyList 类,当然,实际的 ArrayList 类和我们写的 MyList 相比,还是复杂了很多。

用数组这样一种结构来实现 List 接口,具有以下特点:
用数组实现 List 接口,如果要查询 List 中给定下标的元素,只需要使用数组下标就可以直接查到,实现起来非常方便,而且由于数组中,元素的存储空间是连续的,因此通过下标很容易快速对元素进行定位,因此查询效率也很高。

但是,如果使用数组实现 List 接口,则必须要面对一个问题:数组的插入和删除的效率较低。例如,如果要进行数组的插入,有可能要大量移动数组的元素,有可能要进行数组的扩容从而进行大量的内存拷贝的工作。而数组的删除,同样可能意味着要移动大量的数组元素。因此,从这方面来说,数组的插入和删除操作效率比较低。
而 LinkedList 实现 List 接口时,采用的是链表的实现方式。

下面简单介绍一下链表这种数据结构。
最基本的链表结构是这样的:链表由多个节点组成。每个节点分为两个部分:第一个部分用来储存链表的数据,另一个部分用来储存下一个节点的地址。如何来理解这个问题呢?有些寻宝和侦探小说里,常常有这样的情节:要打开一个宝藏,需要分散在不同地方的 n 把钥匙。我们伟大的主人公刚出场的时候,往往手上握有一条线索。通过这条线索,能够找到一个装有钥匙的盒子,并且发现,在装着钥匙的装帧精美的盒子中,会发现寻找下一把 钥匙需要的线索。就这样历经千辛万苦,最后终于获得宝藏。

在这种情形中,如果我们把钥匙当做数据,那么每个盒子就可以当做一个节点:在节点中,一部分放着数据,另一部分指向下一个节点。也就好像是,盒子中装着钥匙,并且装着发现下一个盒子的线索。我们可以用图来表示这种情况。例如,假设有如下代码:

List list = new LinkedList(); list.add("hello");
list.add("world");
list.add("java");
list.add("study");

上面的代码创建了一个 LinkedList,在内存中的图像如下:
file
此时,如果调用 get(3)方法,则会由 hello 节点开始,先从 hello 节点找到 world 节点, 再从 world 节点找到 java 节点,再从 java 节点找到 study 节点。因此在查询方面,相对于数组直接使用下标,链表实现的 LinkedList,在查询方面效率较低。
而如果要进行插入操作,LinkedList 就会有比较明显的优势:因为 LinkedList 不需要进行数据内容的复制。例如,假设运行了如下代码:
list.add(1, “test”);
则内存中会进行下面的操作:

  1. 创建一个新节点。
    file
  2. 修改 hello 和 test 的指针指向即可。
    file
    与之类似的,使用链表进行删除也只需要改动某个指针的指向即可。例如,假设运行了如下代码:
    list.delete(2);
    file

由上面的例子可知,相对使用数组实现 List,使用链表实现 List 中的插入和删除功能, 由于没有数组扩容以及移动数据等问题,因此效率要远远高于使用数组实现。

ArrayList 和 LinkedList 之间的区别如下表:

实现方式    特点

ArrayList 数组实现 增删慢,查询快
LinkedList 链表实现 增删快,查询慢

4.3.2 Vector

Vector 是JDK1.0 遗留下的产物。Vector 同样实现了 List 接口,而且也是使用数组实现。
Vector 和 ArrayList 之间的比较如下:

实现方式 特点
ArrayList 数组实现-轻量级,速度快,线程不安全
Vector 数组实现-重量级,速度慢,线程安全

这两个类实现的方式都采用数组实现,所不同的是,Vector 为了保证线程安全,采用了重量级的实现方式。在 Vector 中,所有的方法都被设计为了同步方法。这样,当多线程共同访问同一个 Vector 对象时,不会产生同步问题,但却牺牲了访问的效率。
而 ArrayList 中所有方法并没有被作成同步方法,因此访问效率较快。当然,当多线程同时访问同一个 ArrayList 对象时,可能会造成线程的同步问题。
关于线程的同步,在本书线程的章节中有更加详细的描述,请读者参考。

5 Set

5.1 Set 特点和基本操作

就像之前提到的一样,Set 接口的特点是元素不可以重复,无顺序。具体例子不再赘述。那 Set 接口有哪些基本操作呢?Set 接口中所有的操作都继承自 Collection 接口,也就是说,Set 接口没有自己特有的操作,其所有操作都来源于父接口 Collection。因此,它具有 Collection 接口中定义的那些诸如 add、remove 等方法。
特别要注意的是,由于 Set 集合中的元素没有顺序,因此 Set 集合中的元素没有下标的概念。因此,和 List 接口不同,Set 接口中没有定义与下标相关的操作。
Set 接口相关的内容请参考对 Collection 接口的描述。

5.2 遍历

与 List 接口一样,我们先介绍一个 Set 接口的实现类,HashSet。我们利用这个类来测试 Set 接口的遍历。
Set 接口中没有跟下标相关的方法,也就是说,Set 接口中没有类似 List 接口中的 get方法,因此,无法使用跟下标紧密联系的 for 循环遍历。
但是,Set 接口可以使用迭代遍历。Collection 接口中定义了 iterator 方法,因此 Set 接口中也包含了这个方法。对于 Set 集合来说(尤其是 JDK1.5 以前的版本),只能采用迭代器的方式来遍历。
示例代码如下:

public class TestSet {
public static void main(String[] args) { Set set = new HashSet(); set.add("hello");
set.add("world");
set.add("java");
//加入重复元素是,add 方法会返回 false

set.add("hello");

//迭代遍历

Iterator iter = set.iterator();
while(iter.hasNext()){
    Object value = iter.next(); System.out.println(value);
    }
}

要注意的是,迭代遍历输出的结果为:
hello java world
注意到对 set 调用了两次 add(“hello”)方法,但是输出结构只有一个 hello 字符串。同时,
注意到输出结果的排列顺序与加入 set 的顺序完全无关。这就是 Set 集合的特点:元素无顺序,不可以重复。

5.3 实现类

对于 Set 集合的基本操作,相对而言比较容易掌握。对于 Set 接口而言,比较难掌握的地方在于 Set 接口的实现类相关内容。下面这部分内容是学习 Set 接口的重点。

5.3.1 HashSet

HashSet 实现了 Set 接口,因此要求元素不可以重复。那么,HashSet 是怎么来判断元素是否可以重复的呢?
我们首先看下面这个代码的例子:

class Student{ private int age;
private String name;

public Student() {
}

public Student(String name, int age) { 
    this.name = name;
    this.age = age;
}

public int getAge() { 
    return age;
}
public void setAge(int age) { 
    this.age = age;
}

public String getName() { 
    return name;
}
public void setName(String name) { 
    this.name = name;
}
public String toString(){ 
    return name + " " + age;
}

}
public class TestStudent{
    public static void main(
        String args[]){ Set set = new HashSet();
        Student stu1 = new Student(“Tom”, 18);
        Student stu2 = new Student(“Tom”, 18);
        set.add(stu1);
        set.add(stu2); 
        System.out.println(set.size());
    }
}

看上述代码。我们创建了两个 Student 对象,这两个对象具有相同的属性。根据 Set 接口的含义,Set 集合中不应该有内容重复元素,因此我们希望调用了两次 add 方法之后,set 的长度依然为 1。然而运行结果却是 2。问题出在哪儿呢?首先来说,要判断两个对象内容是否相等,会调用对象的 equals 方法。而 Student 类中没有覆盖 equals 方法,因此 Student 类中的 equals 方法来源于 Object 类, 判断的是引用中保存的地址是否相等。显然,stu1 和stu2 这两个引用分别指向了一个 Student对象,这两个对象地址不相同,因此用 equals 方法判断,结果为 false。
为了能让 Student 类能够正确的进行判断,我们应该为 Student 类覆盖 equals 方法。示 例代码如下:

public boolean equals(Object obj) { 
    if (this == obj) return true;  
    if (obj == null) return false;
    if (getClass() != obj.getClass()) return false;
    Student stu = (Student) obj;
    if (this.age == stu.age && this.name.equals(stu.name)){ 
        return true;
    }else {
        return false;
    }
}

覆盖了 equals 方法之后,再次运行。但是,运行结果还是 2!这次问题又出在哪里呢? 这里面涉及到了 HashSet 的实现机制:Hash 算法。下面我们简单的来介绍一下 Hash 算
法的原理。
在 Object 类中,有一个 hashCode 方法,这个方法的签名如下:
public int hashCode()
这是一个 Object 类中定义的公开方法,意味着所有对象中都具有这个方法。这个方法没有参数,返回值为一个 int 类型的数值。
在我们把一个对象放到HashSet 中时,HashSet 的add 方法会调用对象的hashCode 方法。假设,我们的 HashSet 的大小为 4,为这四个位置设置下标为 0~3。内存中情况如下:

0
1
2
3

然后,假设调用 add 方法。假设我们有三个对象:str1、str2、str3 三个不同的字符串对象,假设对这三个对象调用 hashCode 方法的返回值为 96、99、100。
调用四次 add 方法如下:

set.add(str1);
set.add(str2);
set.add(str3);
set.add(str1);

在第一个 add 方法中,会调用 str1 的 hashCode 方法,返回值为 96。str1 对象在 HashSet中的位置,是根据这个整数 96 对数组长度取模,计算出来的。由于 96%4=0,因此会把 str1 放入下标为 0 的位置,如下图:

0 <- str1
1
2
3

在第二个 add 方法中,同样会调用str2 的 hashCode 方法,返回值为 99。由于 99%4=3, 因此会把 str2 放入下标为 3 的位置,如下图:

0 <- str1
1
2
3 <- str2

在第三个 add 方法中,会调用 str3 的 hashCode 方法,返回值为 100。由于 100%4=0, 但是下标为0 的位置已经有了一个str1 元素。此时,就产生了hashCode 冲突。当产生hashCode 冲突时,HashSet 会调用 equals 方法进行判断。这是,由于 str1.equals(str3)返回值为false, 这两个对象的值不相等,因此 str3 同样会被加入到 HashSet 中。示意图如下:

0 <- str1 <- str3
1
2
3 <- str2

在第四次调用 add 方法时,会再次调用 str1 的 hashCode 方法,返回值为 96。这时,由于 96%4=0,此时产生了 hashCode 冲突。而这时,HashSet 会调用 equals 方法进行判断。由于判断的结果是返回 true,因此 HashSet 认为这是重复元素,从而不会把 str1 对象再次加入Set,从而避免了重复元素。
示意图如下:

0 <- str1 <- str3
1
2
3 <- str2

从上述我们对 Hash 算法的描述中,可以看出 HashSet 只有在 hashCode 返回值冲突的时候才会调用 equals 方法进行判断。也就是说,两个对象,如果 hashCode 没有冲突,HashSet 就不会调用 equals 方法判断而直接认为这两个对象是不同的对象。
而对我们自己写的 Student 类调用 hashCode 方法时,由于 Student 类没有覆盖 Object 类中的 hashCode 方法,因此得到的返回值是 Object 类中的 hashCode 方法返回值。参考下面的代码:

Student stu1 = new Student("Tom", 18); 
Student stu2 = new Student("Tom", 18); 
System.out.println(stu1.hashCode()); 
System.out.println(stu2.hashCode()); 

程序输出结果如下:

33263331
6413875

可以看出,虽然这两个对象的值相同,并且也覆盖了 equals 方法,但是 hashCode 方法返回值并不相同。这样,HashSet 就认为这两个对象是两个不同的对象,直接把这两个对象放入 HashSet。但是这样一来,就破坏了“Set 中的元素不可重复”这个原则。
那为什么 Student 类有这个问题,而String 类没有这个问题呢?因为String 类是 Sun 公司类库的一部分,在 Sun 公司提供 String 类的时候,就为 String 类提供了正确的 hashCode方法的实现。从而保证了,相同字符串对象,调用 hashCode 方法的返回值都是相同的。 而 Student 类是我们自己写的,这个类中没有覆盖 hashCode 方法,因此调用的 hashCode方法来源于 Object 类。Object 类中的方法不能够满足我们的要求,无法保证相同的对象返回的 hashCode 相同。
那怎么解决这个问题呢?我们应该从 Student 类本身入手,应该在 Student 类中覆盖hashCode 方法。例如,在 Student 类中添加如下方法:

public int hashCode(){
    return age + name.hashCode();
}

这样,就能保证,当两个 Student 对象的age 和name 属性的值都相同时,返回的 hashCode值必定相同。因此,应当这样覆盖 hashCode:相同对象的 hashCode()返回值应当相同。

接下来考虑下面的情况。如果我们把 Student 类中的 hashCode 的覆盖写成下面的形式:
public int hashCode(){ return 0; }
这样是否能满足 hashCode 方法的要求呢?
这样的实现,从结果上来说,是对的。由于任何对象返回的 hashCode 值均为 0,因此符合之前所说的:相同对象的 hashCode 相同。但是这样的实现也有问题:由于任何情况之下返回的 hashCode 值都为 0,因此在往 HashSet 中放入对象时,每次都会产生 hashCode 的冲突,从而每次调用 add 方法都必须要调用 equals 方法比较。而第一个 hashCode 的实现,不同对象造成的 hashCode 冲突的可能性要小得多,因此调用 equals 方法的次数也会少很多。
因此,基于性能方面的考虑,不同的对象 hashCode 返回值应当尽量不同。

总结一下,如果要正常使用 HashSet 存放对象,为了保证对象的内容不重复,则要求这个对象满足:

  1. 覆盖 equals 方法。要求相同的对象,调用 equals 方法返回 true。
  2. 覆盖 hashCode 方法。要求,相同对象的 hashCode 相同,不同对象的 hashCode 尽量不同。

完整代码如下:

import java.util.*;

class Student{
    private String name; private int age;

    public Student() {
    }
    public Student(String name, int age) {
        this.name = name;
    this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) { 
        this.age = age;
    }

    public int hashCode() {
        return age + name.hashCode();
    }

    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
            Student stu = (Student) obj;
        if ( (this.age == stu.age) && (this.name.equals(stu.name)) ){
            return true;
        }else{
            return false;
        }
    }

    public String toString(){ 
        return name + " " + age;
    }
}

public class TestHashSet {
    public static void main(String[] args) { 
        Set set = new HashSet();
        set.add(new Student("Tom", 18));
        set.add(new Student("Jim", 20));
        set.add(new Student("Fred", 22));
        set.add(new Student("Tom", 18));

        Iterator iter = set.iterator(); while(iter.hasNext()){
            System.out.println(iter.next());
        }
    }
}

输出结果如下:

Fred 22
Jim 20
Tom 18

注意,add()方法被调用了四次,但是遍历 set 集合时,只读到了三个元素。

5.3.2 LinkedHashSet

HashSet 的特点是元素不可重复且元素无顺序。某些情况下,我们依然需要元素不可以重复,但是希望按照我们加入 Set 的先后顺序来加入这些元素。这个时候,我们就可以使用
LinkedHashSet。例如下面的例子:

import java.util.*;
public class TestLinkedHashSet {
    public static void main(String args[]){

        Set set = new LinkedHashSet(); set.add("hello");
        set.add("world");
        set.add("java");
        set.add("hello");

        Iterator iter = set.iterator(); while(iter.hasNext()){
        System.out.println(iter.next());
        }
    }
}

输出结果如下:

hello world java

我们可以看到,字符串的打印顺序和它们添加到 LinkedHashSet 中的顺序是一致的。同时,hello 字符串被添加了两次,但只打印了一次。
要注意的是,如果要使用 LinkedHashSet 的话,也必须正确的覆盖对象的 hashCode 和
equals 方法。

6 Map

6.1 Map 特点和基本操作

Map 接口与 Collection 接口不同,这个接口的元素是“键值对”。其中,键值对的特点

是:键不可以重复,值可以重复。
之前解释过,所谓“键值对”,可以理解成一种一一对应的关系。在这种关系中,我们可以通过“键”来找到特定的值。例如,如果把举办世界杯的年份当做键,把该年获得世界杯冠军的球队作为值,则这就形成了一个典型的键值对的关系。在这个关系中,我们可以通过年份查询对应年份的世界杯冠军,这就是“通过键,找到对应的值”的操作;并且举办世界杯的年份不会有重复,而不同年份的世界杯冠军有可能相同,这就对应着“键不可以重复, 值可以重复”。
Map 接口中的一些基本操作罗列如下:

  • Object get(Object key)
    这个方法完成的功能是,通过键对象 key,来找到相应的值对象。
  • put(Object key, Object value)
    这个方法是把一个键值对放入 Map 中。如果键不存在,则在 Map 中新增一个键值对。如果键已存在,则把新值替换旧值。例如,有如下代码:
System.out.println(map.get(“2002”)); map.put(“2002”, “Brazil”); System.out.println(map.get(“2002”)); map.put(“2002”, “China”); System.out.println(map.get(“2002”));

在第一个输出语句中,由于 Map 中不存在以 2002 作为键的键值对,因此第一个输出语句输出为 null。
之后,调用 put 方法。此时,由于 Map 中不存在 2002 这个键,因此会在 Map 中增加一个新的键值对。第二个输出语句就会输出“Brazil”。
之后,再次调用 put 方法。此时,由于在 Map 中 2002 这个键已经存在,因此会用新值“China”替换旧值“Brazil”。于是,第三个输出语句就会输出“China”。

  • remove(Object key)
    这个方法根据一个键,删除一个键值对。
  • Set keySet()
    这个方法返回所有键的集合。由于在 Map 中,键没有顺序,且不可以重复,因此所有的键对象组成的就是一个 Set。也就是说,keySet 方法返回的是一个 Set,这个
    Set 就是所有键对象的集合。
  • Collection values()
    values 方法返回类型是一个 Collection,返回的是所有值对象的集合。
  • containsKey / containsValue
    这两个方法用来判断在 Map 中键是否存在,或者值是否存在。
  • size()
    这个方法返回 Map 中键值对的个数
  • isEmpty()
    判断 Map 是否为空
  • clear()
    清空 Map
  • entrySet
    这个方法返回值类型是一个 Set 集合,集合中放的是 Map.Entry 类型。这个方法是

用来做键值对遍历的,在讲解遍历的时候还会给大家讲到。

6.2 遍历

与之前一样,在真正开始讲解遍历之前,首先先使用一个 Map 接口的实现类:HashMap。创建相应的 HashMap 对象,并放入一些初始值,如下面代码所示:

import java.util.*; 

public class TestMap {
    public static void main(String args[]){ Map map = new HashMap();

        map.put("2006", "Italy");
        map.put("2002", "Brazil");
        map.put("1998", "France");
        map.put("1994", "Brazil");
    }
}

在这个 Map 的基础上,我们开始对 Map 进行遍历。
由于 Map 管理的是键值对,因此对于 Map 而言,有多种遍历的方式:键遍历、键值遍历、利用 Map.Entry 进行键值遍历。

6.2.1 键遍历与键值遍历

键遍历指的是遍历所有的键。键遍历的实现非常简单:通过调用 Map 接口中的 keySet 方法,就能获得所有键的集合。然后,就可以像遍历普通 Set 一样遍历所有键对象的集合。键遍历参考代码如下:

Set set = map.keySet(); 
Iterator iter = set.iterator(); 
while(iter.hasNext()){
    System.out.println(iter.next());
}

键遍历输出结果如下:

2006
1998
2002
1994

可以看到,键遍历输出了集合中所有的键,并且,键并没有顺序。

在键遍历的基础上更进一步,能够遍历所有的键值对。思路如下:
利用键遍历能够遍历所有的键,而在遍历键的时候,可以使用 get 方法,通过键找到对应的值。键值遍历的参考代码如下:

Set set = map.keySet(); 
Iterator iter = set.iterator(); 
while(iter.hasNext()){
    Object key = iter.next();
    Object value = map.get(key); System.out.println(key + "--->" + value);
}

键值遍历的结果如下:

2006--->Italy 1998--->France
2002--->Brazil 1994--->Brazil

可以看到,键值遍历时能够输出键值对这种一一对应的关系。

6.2.2 值遍历

除了键遍历以及键值遍历之外,Map 接口还有一种遍历方式:值遍历。值遍历表示的是遍历 Map 中所有的值对象。与键遍历类似,我们对 Map 进行值遍历的思路也很简单:首先利用 Map 的 values()方法获得 Map 中所有值的集合。需要注意的是,values()方法返回的是一个 Collection 类型的对象,因此,应当用迭代遍历的方式,遍历这个 Collection。参考代码如下:

Collection conn = map.values(); 
Iterator iter = conn.iterator(); 
while(iter.hasNext()){
    System.out.println(iter.next());
}

这样,我们就遍历了 Map 中的所有值。输出结果如下:

Italy France Brazil Brazil
6.2.3 利用 Map.Entry 进行遍历

在 Map 接口中,有一个方法叫做 entrySet。这个方法返回一个 Set 集合,这个集合中装的元素的类型是 Map.Entry 类型。
Map.Entry 是 Map 接口的一个内部接口。这个接口封装了 Map 中的一个键值对。在这个接口中,主要定义了这样几个方法:

  • getKey() : 获得该键值对中的键
  • getValue(): 获得该键值对中的值
  • setValue():修改键值对中的值
    因此,利用 Map.Entry 也可以进行遍历。相关代码如下:

    Set set = map.entrySet(); 
    Iterator iter = set.iterator(); 
    while(iter.hasNext()){
    Map.Entry entry = (
    Map.Entry) iter.next(); 
    System.out.println(entry.getKey()   +   "-->"  +
    entry.getValue());
    }

注意,在赋值的时候,应当把 iter.next 的返回值强转成 Map.Entry 类型才可以。结果如下:
2006-->Italy 1998-->France 2002-->Brazil 1994-->Brazil
可以看到,用 Map.Entry 进行遍历,以及使用 keySet()方法以及 get()方法进行键值,这两种遍历的结果是一样的。

6.3 实现类

Map 接口主要的实现类就是 HashMap 和 LinkedHashMap,此外还有一个使用较少的 Hashtable。
HashMap 的特点是:在判断键是否重复的时候,采用的算法是 Hash 算法,因此要求作为 HashMap 的键的对象,也应该正确覆盖 equals 方法和 hashCode 方法。
LinkedHashMap 和 HashMap 之间的区别有点类似于 LinkedHashSet 和HashSet 之间的区别:LinkedHashMap 能够保留键值对放入 Map 中的顺序。
例如, 如果我们把上一小节的例子中, Map 接口的实现类由 HashMap 改为
LinkedHashMap,修改后的完整的代码如下:

import java.util.*;
public class TestLinkedHashMap {
    public static void main(String args[]){ 
        Map map = new LinkedHashMap(); map.put("2002", "Brazil");
        map.put("1998", "France");
        map.put("1994", "Brazil");
        map.put("2006", "Italy");

        Set set = map.keySet(); 
        Iterator iter = set.iterator(); 
        while(iter.hasNext()){
            Object key = iter.next(); 
            Object value = map.get(key);
            System.out.println(key + "--->" + value);
        }
    }
}

键值遍历之后,输出结果如下:
2002--->Brazil 1998--->France 1994--->Brazil 2006--->Italy
可以看到,进行键值遍历时,输出的顺序,与我们在 Map 中进行 put 的顺序相同。这就是 LinkedHashMap 的特点,这个类能够保留键值对放入 Map 中的顺序。

Hashtable 也是 Map 接口的一个实现类。HashMap 和 Hashtable 之间的区别罗列如下:

null 值处理
HashMap 轻量级,速度快,线程不安全 允许 null 作为键/值
Hashtable 重量级,速度慢,线程安全 null 作为键/值时会抛出异常

要注意,HashMap 和 Hashtable 有两方面的区别。一方面,这两个实现类一个是重量级, 一个是轻量级。这类似于 ArrayList 和 Vector 的区别,也就是说,Hashtable 中的所有方法都是同步方法,因此是线程安全的。另一方面,在于对 null 值的处理。例如,有如下代码:
Map map = new HashMap(); map.put(“2010”, null);
上面的代码创建了一个 HashMap 作为 Map 接口的实现类,并增加了一个“2010:null” 的键值对。在这个键值对中,2010 是键,null 作为值。
而如果把实现类改为 Hashtable,则上面的代码会抛出一个异常。也就是说,不允许把null 作为键值对中的键或者值。

7 Comparable 与排序

7.1 Collections 类与 Comparable

在 java.util 包中,提供了一个 Collections 的类(注意这个类的名字,与我们的 Collection 接口只相差最后一个字母 s)。这个类中所有的方法都是静态方法,也就是说,Collections类所有方法都能够通过类名直接调用。这个类为我们提供了很多使用的功能,例如:sort 方法,这个方法能够对 List 进行排序。再例如,max 方法可以找到集合中的最大值,而 min 方法可以找到集合中的最小值,等等。
调用 Collections.sort 方法,可以对一个 List 进行排序。与在接口那一章中讲述的一样, 要想对 List 进行排序,就要求 List 中的对象实现 Comparable 接口。具体应该如何实现请参考“接口”的章节。

7.2 TreeSet 与 TreeMap

Set 接口有一个子接口:SortedSet,这个接口的特点是:元素不可重复,且经过排序。这个接口有个典型的实现类:TreeSet。要注意的是,因为TreeSet 中要对对象进行排序,因此要求放入 TreeSet 接口中的对象都必须实现 Comparable 接口。

例如,在讲述接口的知识时,我们曾经介绍过如何实现 Comparable 接口。我们利用
Student 类实现 Comparable 接口。在实现 Comparable 接口时,必须要实现 compareTo 方法, 表示对学生进行排序。排序时,我们按照如下排序规则:对学生的年龄进行排序,年龄较小的学生排前面,年龄较大的学生排后面。Student 类的代码如下:

class Student implements Comparable<Student>{ 
    int age;
    String name;
    public Student() {
    }
    public Student(String name, int age) { 
        this.name = name;
        this.age = age;
    }
    public int compareTo(Student stu){ if (
        this.age > stu.age){
        return 1;
    }else if (this.age < stu.age){ return -1;
    }else {
        return 0;
    }
}
}

然后,就可以把学生对象放入 TreeSet 中。代码如下:

public class TestTreeSet {
    public static void main(String args[]) { 
        Set set = new TreeSet();
        set.add(new Student("Tom", 18));
        set.add(new Student("Jim", 17));
        set.add(new Student("Jerry", 20));

        Iterator iter = set.iterator(); 
        while(iter.hasNext()){
        Student stu = (Student) iter.next(); 
        System.out.println(stu.name + " " + stu.age);
        }
    }
}

输出结果如下:

Jim 17
Tom 18
Jerry 20

可以看出,遍历输出时,按照年龄的顺序,从小到大依次输出。这说明了在 TreeMap 内部进行排序时,使用了我们定义的 compareTo 方法比较两个元素的大小。

特别要注意的一点,TreeSet 进行比较时,会把利用 compareTo 方法比较时,返回值为 0 的两个对象,当做是相同对象。例如下面的代码:

set.add(new Student("Tom", 18));
set.add(new Student("Jim", 18));

上面的两行代码,在 set 中放入了两个对象。这两个对象用compareTo 进行比较时,由于两个对象的 age 属性相同,因此会返回 0。则 TreeSet 认为这两个元素是相同元素,所以在 TreeSet 中只保留了一个对象。
完整代码如下:

public class TestTreeSet {
    public static void main(String args[]) {

        Set set = new TreeSet(); set.add(new Student("Tom", 18));
        set.add(new Student("Jim", 18));

        Iterator iter = set.iterator(); while(iter.hasNext()){
        Student stu = (Student) iter.next(); System.out.println(stu.name + " " + stu.age);
        }
    }
}

输出结果如下:

Tom 18

可以看到,虽然调用了两次 add 方法,但两个元素被认为是相同元素,因此在 TreeSet 中只有一个元素。

与之类似的,Map 接口有一个子接口:SortedMap。这个接口的特点是:对 Map 的键进行了排序。这个接口的典型实现类是 TreeMap,如果要把某个键值对放入 TreeMap,则要求键对象必须实现 Comparable 接口。
我们把前一小节中世界杯的例子,使用TreeMap 来改写。要注意的是,在这个例子中,
Map 的键是 String 类型,这个类型是由 Sun 公司提供的,已经实现了 Comparable 接口。代码如下:

import java.util.*;
public class TestTreeMap {
    public static void main(String args[]){
        Map map = new TreeMap(); map.put("2002", "Brazil");
        map.put("1998", "France");
        map.put("1994", "Brazil");
        map.put("2006", "Italy");

        Set set = map.keySet(); Iterator iter = set.iterator(); 
        while(iter.hasNext()){
            Object key = iter.next(); Object value = map.get(key);
            System.out.println(key + "--->" + value);
        }
    }
}

运行结果如下:

1994--->Brazil 1998--->France 2002--->Brazil
2006--->Italy

可以看到,遍历 Map 时,对键进行了排序,按照键对象的排序结果,依次输出 Map 中的键值对。

8 5.0 新特性:foreach 循环

在 JDK5.0 中,Sun 公司对集合框架部分进行了比较大的修改和调整,为集合框架增加了很多新的特性。首先介绍一下一个简单的新特性:foreach 循环。
foreach 循环主要要解决的是遍历的问题。对于 List 来说,我们可以采用 for 循环遍历,
而对于 Set 而言,我们只能采用迭代遍历。相对 for 循环遍历而言,迭代遍历的代码比较繁琐和复杂。例如,对于一个 Set 而言,采用迭代遍历的代码如下:

Iterator iter = set.iterator();
while(iter.hasNext()){
    Object value = iter.next();
    System.out.println(value);
}

为了简化遍历的代码,在 5.0 中引入了 foreach 循环。基本语法如下:

for(变量 : 集合){ 循环体;
}

这段代码表示,遍历整个集合,每次迭代时都把集合中的一个元素赋值给 foreach 循环中的变量,并执行循环体。
例如,上面采用迭代遍历的代码,可以写成:

for(Object value : set){ System.out.println(value);
}

这段代码表示,遍历 set 集合,在每次迭代时把 set 集合的元素赋值给 value 变量。可以看出,使用 foreach 循环遍历,语法比迭代遍历要简洁的多。
实际上,foreach 循环遍历和迭代遍历是完全等价的。在写代码时,如果程序员使用了 foreach 循环的语法,那么 5.0 的编译器会把 foreach 循环自动的翻译成对应的迭代遍历。那么什么样的集合能够用 foreach 循环来遍历呢?在 Java5.0 中, 只要是实现了java. lang.Iterable 接口的集合对象,都能使用 foreach 方式来遍历。前面介绍的所有 Collection 接口的实现类,都符合这个特点。
此外,foreach 循环还能够用来遍历数组。例如下面的代码:

String[] ss = new String[]{“hello”, “world”, “java”}; 
for(String obj : ss){
    System.out.println(obj);
}

上面的代码演示了如何使用 foreach 循环来遍历数组。

9 5.0 新特性:泛型

泛型本身是一个非常大的话题,要彻底的掌握以及用好泛型,并不是一朝一夕的事情。在本书中,我们会介绍泛型最常用、最需要掌握的概念和语法。

在正式介绍泛型知识之前,我们首先谈一下 Java5.0 以前集合框架的缺点。在 5.0 以前,ArrayList 这个类相比直接使用数组来管理多个对象而言,具有很多优势,例如方法更多, 使用更加方便等等。但是 ArrayList 也有缺点。例如,如果定义三个类:Animal、Dog 和 Cat 类如下:

abstract class Animal{ 
    abstract public void eat();
}
class Dog extends Animal{ 
    public void eat(){
        System.out.println("dog eat bones");
    }

    public void bark(){ 
        System.out.println("wang");
    }
}

class Cat extends Animal{ 
    public void eat(){
        System.out.println("Cat eat fish");
    }
    public void miaow(){ System.out.println("miao");
    }
}

如果创建一个 Dog 类型的数组,并且对每个对象调用 bark 方法,代码如下:

Dog[] dogs = new Dog[2];
dogs[0] = new Dog();
dogs[1] = new Dog();
for(int i = 0; i<dogs.length; i++){
    dogs[i].bark();
}

由于这是一个 Dog 数组,因此数组中的每一个元素都是 Dog 类型,从而可以直接把数组中的元素赋值给一个 Dog 类型的变量。而对这个变量,则可以直接调用 bark 方法。

而如果不是一个数组,使用一个 ArrayList,同样对 ArrayList 中每个对象调用 bark 方法, 则代码如下:

List list = new ArrayList(); list.add(new Dog());
list.add(new Dog());
list.add(new Dog());
for(int i = 0; i<list.size(); i++){ 
    Dog d = (Dog) list.get(i);
    d.bark();
}

由于 ArrayList 的 get 方法返回的是一个 Object 类型,为了调用 bark 方法,还必须进行强制类型转换。这就是集合不如数组的第一个地方:存入集合的是 Dog 类型,而取出来的时候则变成了 Object 类型,需要进行强制类型转换。
此外,如果在 list 中调用了下面的代码:
list.add(new Cat());
这个程序在编译时没有错误,但是在运行时会产生一个 ClassCastException。这是集合不如数组的第二个地方:使用集合时,由于一个集合中能够装多个类型的对象,因此在使用时很有可能发生类型转换异常。
为什么会有这两个问题呢?原因很简单:ArrayList 为了设计的更加通用,其内部保存
数据的时候,保存数据的时候都采用的是 Object 类型。因为只有设计成这样,ArrayList 才能用来保存所有的 Java 对象。但是,如果把 ArrayList 设计成这样的话,就会造成之前的两个问题:我们无法象定义某个类型的数组那样,定义一个专门用于存放某个类型对象的集合。因此,我们称传统的集合对象是:类型不安全的。
5.0 引入的泛型机制能够很好的解决我们上面所说的问题。

9.1 泛型的基本使用

使用 5.0 的泛型机制非常简单。例如,我们希望创建一个只能用来保存 Dog 对象的 List, 可以使用如下代码:

List<Dog> list = new ArrayList<Dog>();

注意,跟原有的代码相比,在 List 接口和 ArrayList 后面,都有个后缀:。这表明,创建的 ArrayList 只能够放置 Dog 类型。此时,如果对 list 放入非 Dog 类型对象,则会产生一个编译错误,示例如下:

list.add(new Dog()); //OK list.add(new Cat()); //!编译错误

这样,就能保证 ArrayList 中所有的对象都是 Dog 类型的对象。于是,get 方法返回值就可以确定,一定是 Dog 类型,也就省略了强制类型转换的步骤。所以原有的代码就可以修改成:

for(int i = 0; i<list.size(); i++){ 
    Dog d = list.get(i);
    d.bark();
}

注意到在调用 get 方法的时候,没有进行强制类型转换。
现在我们更仔细的探讨一下上面的这段程序。在 5.0 以后的 Java 文档中,List 接口定义为:List,其中,E 就表示 List 的泛型。

上面的代码中,我们定义 list 变量的类型为 List,这就意味着,我们把 E 类型设置为 Dog 类型。在 List 中定义的 get 方法,其声明为:
E get(int index)
其返回值类型为 E。由于我们设置了 E 为 Dog 类型,因此,我们对我们定义的 list 变量调用 get 方法,其返回值类型为 Dog 类型。
对 Set 集合使用泛型的方法,和 List 接口使用泛型的方法类似,在此不再赘述。
此外,考虑 foreach 循环。如果不使用泛型的话,foreach 循环每次迭代的时候,只能确定元素是 Object 类型,因此 foreach 循环只能写成:

for(Object obj : list){
…
}

而是用了泛型以后,由于能够确定集合中元素的类型,因此 foreach 循环可以写成:

for(E e : list){
…
}
例如,上面遍历包含 Dog 的 list 的代码,就可以修改成:
```jav
```a
for(Dog d : list){ d.bark();
}

可以看出,泛型与 foreach 循环结合,大大简化了遍历集合的代码。
除了可以对 List 和 Set 使用泛型,对 Map 类型也可以使用泛型。但是要注意的是,Map 由于管理的是键值对,键有一个类型,值也有一个类型,因此 Map 的泛型需要有两个参数。示例代码如下:

Map<Integer, String> map = new HashMap<Integer, String>(); 
map.put(2002, “Brazil”);
map.put(1998, “France”); Set<Integer> set = map.keySet(); for(Integer i : set){
System.out.println(i    + “” + map.get(i) );
}

请注意,map 的 keySet 方法返回值为一个 Set类型。另外,上述代码中,map 对象的键类型被设置为 Integer,而我们在调用 put 方法的时候采用了 2002,1998 这样的 int 数据,根据 JDK5.0 中自动封箱的语法,int 类型的数据会被自动封装为Integer 类型的对象。

9.2 泛型与多态

请看下面的例子:

List<Dog> dogList = new ArrayList<Dog>(); List<Animal> aniList = dogList; //! 编译出错!

这两行代码中,第一行编译正确,第二行编译出错。
在第一行代码中,把一个 ArrayList直接赋值给一个 List类型的引用。在这个赋值过程中,类型里有多态(把 ArrayList 赋值给 List),但是泛型是一样的(均是 Dog 的泛型)。
在第二行代码中,把一个 List赋值给一个 List,这样赋值是错误的!在这个过程中,类型中没有多态(List 是相同的),而泛型有多态(一个是 Dog 的泛型,一个是 Animal 的泛型)。这句话会导致一个编译错误!
请记住这个结论:类型可以有多态,但是泛型不能够有多态!
为什么会有这么个结论呢?假设可以把 dogList 直接赋值给 aniList,则可以对 aniList调用 add 方法:
aniList.add(new Cat());
这句代码能够编译通过,因为根据泛型,add 方法接受一个 Animal 类型的参数,而 Cat 对象能够当做一个 Animal 对象。
而事实上,对象是被添加到了 dogList 当中,而这个 list 中只能存放 Dog 对象,不能存放 Cat 对象,这就出现了前后矛盾的问题。
为了避免这样的问题出现,因此,在 java 中,泛型不能有多态。不同泛型的引用之间不能相互赋值。

这个结论可以扩展到把泛型用在函数参数上。例如,写一个 playWithDog 方法如下:

public static void playWithDog(List<Dog> dogs){ 
    for(Dog d : dogs){
        d.bark();
    }
}

这个函数接受一个参数,这个参数的类型是 List类型。根据我们的结论,类型可以有多态,因此我们可以给这个函数传递一个 ArrayList类型的对象作为参数,也可以给这个函数传递一个 LinkedList类型的对象作为参数。但是,泛型上没有多态。假设有一个类 Courser 表示猎狗:
class Courser extends Dog{}
在传递参数给 playWithDog 的时候,不能够传递一个 ArrayList类型的对象作为实参。这同样是因为,不同泛型的引用之间不能相互赋值。

9.3 自定义泛型化类型

现在我们定义一个类,用来表示“一对”这个概念。如果不用泛型的话,示例代码如下:

class Pair{
    private Object valueA; private Object valueB;

    public Object getValueA() {

    return valueA;
    }

    public void setValueA(Object valueA) { 
        this.valueA = valueA;
    }

    public Object getValueB() { 
        return valueB;
    }

    public void setValueB(Object valueB) { 
        this.valueB = valueB;
    }

    }

    public class TestPair {

    public static void main(String[] args){ 
        Pair p = new Pair(); p.setValueA(new Dog()); 
        p.setValueB(new Dog());
    }
}

可以看到,Pair 类为了尽可能通用,使用了 Object 类型来保存一对值。但是这样就会有类型方面的问题,例如:
p.setValueA(new Dog()); p.setValueB(new Cat());
这样,这个代码就把一只猫和一条狗硬生生配成了一对,显然,我相信无论是猫还是狗
都不会愿意的。
为此,我们应该考虑让我们的 Pair 类具有更加安全的类型,即:要求对 Pair 类来说,
valueA 属性和 valueB 属性具有相同的类型。为此,我们可以为 Pair 类使用泛型。首先修改 Pair 类的定义:
class Pair<T>
在 Pair 类后面,写一对尖括号,表明 Pair 类要使用泛型。在尖括号中的大写字母 T 称为泛型参数,T 用来标识 Pair 的泛型类型。
然后,把 valueA 和 valueB 的声明也进行修改:
private T valueA; private T valueB;
表明 valueA 和 valueB 属性为 T 类型。
最后,把所有的方法也进行修改:

public T getValueA() { return valueA;
}

public void setValueA(T valueA) { this.valueA = valueA;
}

public T getValueB() { return valueB;
}

public void setValueB(T valueB) { this.valueB = valueB;
}

注意,set 方法的参数以及 get 方法的返回值类型,都为 T 类型。这样,使用泛型的 Pair 类就定义好了。在使用的时候,我们就可以指定泛型:
Pair<Dog> p= new Pair<Dog>();
这就指定了,对于 p 对象来说,泛型 T 被赋值为 Dog 类型。在代码中凡是出现“T” 的地方,都会被“Dog”所取代。
此时,如果对 p 调用 set 方法而给出一个不是 Dog 的类型,则会编译出错。例如:

p.setValueA(new Dog());//OK p.setValueB(new Cat());//!编译出错!

完整的代码如下:

class Pair<T>{ 
    private T valueA; private T valueB;

    public T getValueA() { 
        return valueA;
    }

    public void setValueA(T valueA) { 
        this.valueA = valueA;
    }

    public T getValueB() { return valueB;
    }

    public void setValueB(T valueB) { 
        this.valueB = valueB;
    }

    }

    public class TestPair {
    public static void main(String[] args){ 
        Pair<Dog> p = new Pair<Dog>(); p.setValueA(new Dog()); p.setValueB(new Dog());
        // p.setValueA(new  Cat()); 编译出错!
    }
}

Views: 56

Java面向对象设计

抽象类 VS 接口

Java相比于其他面向对象语言,如C++,设计上有一些基本区别,比如Java不支持多继承。这种限制,在规范了代码实现的同时,也产生了一些局限性,影响着程序设计结构。Java类可以实现多个接口,因为接口是抽象方法的集合,所以这是声明性的,但不能通过扩展多个抽象类来重用逻辑。

在一些情况下存在特定场景,需要抽象出与具体实现、实例化无关的通用逻辑,或者纯调用关系的逻辑,但是使用传统的抽象类会陷入到单继承的窘境。以往常见的做法是,实现由静态方法组成的工具类(Utils),比如java.util.Collections。

设想,为接口添加任何抽象方法,相应的所有实现了这个接口的类,也必须实现新增方法,否则会出现编译错误。对于抽象类,如果我们添加非抽象方法,其子类只会享受到能力扩展,而不用担心编译出问题。
接口的职责也不仅仅限于抽象方法的集合,其实有各种不同的实践。有一类没有任何方法的接口,通常叫作Marker Interface,顾名思义,它的目的就是为了声明某些东西,比如我们熟知的Cloneable、Serializable等。这种用法,也存在于业界其他的Java产品代码中。
从表面看,这似乎和Annotation异曲同工,也确实如此,它的好处是简单直接。对于Annotation,因为可以指定参数和值,在表达能力上要更强大一些,所以更多人选择使用Annotation。

Java 8增加了函数式编程的支持,所以又增加了一类定义,即所谓functional interface,简单说就是只有一个抽象方法的接口,通常建议使用@FunctionalInterfaceAnnotation来标记。Lambda表达式本身可以看作是一类functional interface,某种程度上这和面向对象可以算是两码事。我们熟知的Runnable、Callable之类,都
是functional interface,这里不再多介绍了,有兴趣可以参考:https://www.oreilly.com/learning/java-8-functional-interfaces

还有一点可能让人感到意外,严格说,Java 8以后,接口也是可以有默认方法实现的!

从Java 8开始,interface增加了对default method的支持。Java 9以后,甚至可以定义private default method。Default method提供了一种二进制兼容的扩展已有接口的办法。比如,我们熟知的java.util.Collection,它是collection体系的root interface,在Java 8中添加了一系列default method,主要是增加Lambda、Stream相关的功能。我在专栏前面提到的类似Collections之类的工具类,很多方法都适合作为default method实现在基础接口里面。
你可以参考下面代码片段:

public interface Collection<E> extends Iterable<E> {
     /**
     * Returns a sequential Stream with this collection as its source 
     * ...
     **/
     default Stream<E> sream() {
        return StreamSupport.sream(spliterator(), false);
     }
 }

面向对象设计

谈到面向对象,很多人就会想起设计模式,那些是非常经典的问题和设计方法的总结。不过在那之前还是先夯实一下基础,先来聊聊面向对象设计的基本方面。

我们一定要清楚面向对象的基本要素:封装、继承、多态。

封装的目的是隐藏事务内部的实现细节,以便提高安全性和简化编程。封装提供了合理的边界,避免外部调用者接触到内部的细节。我们在日常开发中,因为无意间暴露了细节导致难缠bug太多了,比如在多线程环境暴露内部状态,导致的并发修改问题。从另外一个角度看,封装这种隐藏,也提供了简化的界面,避免太多无意义的细节浪费调用者的精力。
继承是代码复用的基础机制,类似于我们对于马、白马、黑马的归纳总结。但要注意,继承可以看作是非常紧耦合的一种关系,父类代码修改,子类行为也会变动。在实践中,过度滥用继承,可能会起到反效果。
多态,你可能立即会想到重写(override)和重载(overload)、向上转型。简单说,重写是父子类中相同名字和参数的方法,不同的实现;重载则是相同名字的方法,但是不同的参数,本质上这些方法签名是不一样的,为了更好说明,请参考下面的样例代码:

public int doSomething() {
 return 0;
}

// 输入参数不同,意味着方法签名不同,重载的体现
public int doSomething(Lis<String> srs) {
 return 0;
}
// return类型不一样,编译不能通过
public short doSomething() {
 return 0;
}

这里你可以思考一个小问题,方法名称和参数一致,但是返回值不同,这种情况在Java代码中算是有效的重载吗? 答案是不是的,编译都会出错的。

进行面向对象编程,掌握基本的设计原则是必须的,我今天介绍最通用的部分,也就是所谓的S.O.L.I.D原则。

  • 单一职责(Single Responsibility),类或者对象最好是只有单一职责,在程序设计中如果发现某个类承担着多种义务,可以考虑进行拆分。
  • 开关原则(Open-Close, Open for extension, close for modifcation),设计要对扩展开放,对修改关闭。换句话说,程序设计应保证平滑的扩展性,尽量避免因为新增同
    类功能而修改已有实现,这样可以少产出些回归(regression)问题。
  • 里氏替换(Liskov Substitution),这是面向对象的基本要素之一,进行继承关系抽象时,凡是可以用父类或者基类的地方,都可以用子类替换。
  • 接口分离(Interface Segregation),我们在进行类和接口设计时,如果在一个接口里定义了太多方法,其子类很可能面临两难,就是只有部分方法对它是有意义的,这就破坏
    了程序的内聚性。
    对于这种情况,可以通过拆分成功能单一的多个接口,将行为进行解耦。在未来维护中,如果某个接口设计有变,不会对使用其他接口的子类构成影响。
  • 依赖反转(Dependency Inversion),实体应该依赖于抽象而不是实现。也就是说高层次模块,不应该依赖于低层次模块,而是应该基于抽象。实践这一原则是保证产品代码之
    间适当耦合度的法宝。

OOP原则实践中的取舍

值得注意的是,现代语言的发展,很多时候并不是完全遵守前面的原则的,比如,Java 10中引入了本地方法类型推断和var类型。按照,里氏替换原则,我们通常这样定义变量:
List<String> list = new ArrayLis<>();
如果使用var类型,可以简化为
var list = new ArrayLis<String>();
但是,list实际会被推断为“ArrayList
ArrayList<String> list = new ArrayLis<String>();
理论上,这种语法上的便利,其实是增强了程序对实现的依赖,但是微小的类型泄漏却带来了书写的变量和代码可读性的提高,所以,实践中我们还是要按照得失利弊进行选择,而不是一味得遵循原则。

OOP原则的分析

看看下面这段代码,改编自朋友圈盛传的某
伟大公司产品代码,你觉得可以利用面向对象设计原则如何改进?

public class VIPCenter {
     void serviceVIP(T extend User user>) {
     if (user insanceof SlumDogVIP) {
     // 穷X VIP,活动抢的那种
     // do somthing
     } else if(user insanceof RealVIP) {
     // do somthing
     }
     // ...
 }

这段代码的一个问题是,业务逻辑集中在一起,当出现新的用户类型时,比如,大数据发现了我们是肥羊,需要去收获一下, 这就需要直接去修改服务方法代码实现,这可能会意外影响不相关的某个用户类型逻辑。
利用开关原则,我们可以尝试改造为下面的代码:

public class VIPCenter {
     private Map<User.TYPE, ServiceProvider> providers;
     void serviceVIP(T extend User user) {
        providers.get(user.getType()).service(user);
     }
}

interface ServiceProvider{
    void service(T extend User user) ;
}

class SlumDogVIPServiceProvider implements ServiceProvider{
     void service(T extend User user){
     // do somthing
     }
 }
 class RealVIPServiceProvider implements ServiceProvider{
     void service(T extend User user) {
     // do something
}

上面的示例,将不同对象分类的服务方法进行抽象,把业务逻辑的紧耦合关系拆开,实现代码的隔离保证了方便的扩展。

Views: 34