尚学堂JAVA基础学习笔记

尚学堂JAVA基础学习笔记

写在前面

学习链接:Java 视频教程全集

课件链接:Java课件

第1章 JAVA入门

  • 计算机语言发展史以及未来方向

    • 第一代语言:机器语言
  • 第二代语言:汇编语言

  • 第三代语言:高级语言

  • Java的核心优势

    • JAVA虚拟机是JAVA实现跨平台的核心。
  • Java的各个版本

    2017-6-6 10-12-07.jpg

    • JavaSE(Java Standard Edition):标准版,定位在个人计算机上的应用。

    • JavaEE(Java Enterprise Edition):企业版,定位在服务器端的应用。

    • JavaME(Java Micro Edition):微型版,定位在消费性电子产品的应用上。

    • 雷区:很多人开始会误解为安卓开发就是JavaME,这两个是完全不同的内容。

  • Java的特征和优势

    • 跨平台/可以执行
    • 安全性
    • 面相对象
    • 简单性
    • 高性能
    • 分布式
    • 多线程
    • 健壮性
  • Java应用程序的运行机制

    图片6.png

    • Java首先利用文本编辑器编写 Java源程序,源文件的后缀名为.java;再利用编译器(javac)将源程序编译成字节码文件,字节码文件的后缀名为.class; 最后利用虚拟机(解释器,java)解释执行。
  • JVM、JRE和JDK

    • JVM(Java Virtual Machine)就是一个虚拟的用于执行bytecode字节码的”虚拟计算机”。他也定义了指令集、寄存器集、结构栈、垃圾收集堆、内存区域。JVM负责将Java字节码解释运行,边解释边运行,这样,速度就会受到一定的影响。

      图片8.png

    • Java Runtime Environment (JRE) 包含:Java虚拟机、库函数、运行Java应用程序所必须的文件。

    • Java Development Kit (JDK)包含:包含JRE,以及增加编译器和调试器等用于程序开发的文件

      图片7.png

  • 第一个Java程序的总结和提升

    • 一个源文件可以包含多个类class。
    • 正确编译后的源文件,会得到相应的字节码文件,编译器为每个类生成独立的字节码文件,且将字节码文件自动命名为类的名字且以“.class”为扩展名。
  • 最常用DOS命令

    • cd 目录路径:进入一个目录
    • cd ..:进入父目录
    • dir:查看本目录下的文件和子目录列表
    • cls:清楚屏幕命令
    • 上下键:查找敲过的命令
    • Tab键:自动补齐命令

第2章 数据类型和运算符

  • 注释

    • 单行注释: 使用“//”开头,“//”后面的单行内容均为注释。
    • 多行注释: 以“/*”开头以“*/”结尾,在“/*”和“*/”之间的内容为注释,我们也可以使用多行注释作为行内注释。但是在使用时要注意,多行注释不能嵌套使用。
    • 文档注释: 以“/**”开头以“*/”结尾,注释中包含一些说明性的文字及一些JavaDoc标签(后期写项目时,可以生成项目的API)
  • 标识符

    • 标识符必须以字母、下划线_、美元符号$开头。
    • 标识符其它部分可以是字母、下划线“_”、美元符“$”和数字的任意组合。
    • Java 标识符大小写敏感,且长度无限制。
    • 标识符不可以是Java的关键字。
    • 标识符的使用规范
      • 表示类名的标识符:每个单词的首字母大写,如Man, GoodMan
      • 表示方法和变量的标识符:第一个单词小写,从第二个单词开始首字母大写,我们称之为“驼峰原则”,如eat(), eatFood()
  • Java中的关键字/保留字

    img

  • 变量的分类

    • 局部变量(local variable):方法或语句块内部定义的变量。生命周期是从声明位置开始到到方法或语句块执行完毕为止。局部变量在使用前必须先声明、初始化(赋初值)再使用。
    • 成员变量(也叫实例变量 member variable):方法外部、类的内部定义的变量。从属于对象,生命周期伴随对象始终。如果不自行初始化,它会自动初始化成该类型的默认初始值。
    • 静态变量(类变量 static variable):使用static定义。 从属于类,生命周期伴随类始终,从类加载到卸载。 (注:讲完内存分析后我们再深入!先放一放这个概念!)如果不自行初始化,与成员变量相同会自动初始化成该类型的默认初始值。

    img

  • 变量和常量命名规范(规范是程序员的基本准则,不规范会直接损害你的个人形象):

    1. 所有变量、方法、类名:见名知意
    2. 类成员变量:首字母小写和驼峰原则: monthSalary
    3. 局部变量:首字母小写和驼峰原则
    4. 常量:大写字母和下划线:MAX_VALUE
    5. 类名:首字母大写和驼峰原则: Man, GoodMan
    6. 方法名:首字母小写和驼峰原则: run(), runRun()
  • 基本数据类型(primitive data type)

    7.png

    • 引用数据类型的大小统一为4个字节,记录的是其引用对象的地址!
  • 整型变量/常量

    img

    • Java 语言整型常量的四种表示形式
      • 十进制整数,如:99, -500, 0
      • 八进制整数,要求以 0 开头,如:015
      • 十六进制数,要求 0x 或 0X 开头,如:0x15
      • 二进制数,要求0b或0B开头,如:0b01110011
    • Java语言的整型常数默认为int型,声明long型常量可以后加‘ l ’或‘ L ’ 。
  • 浮点型变量/常量

    img

    • Java浮点类型常量有两种表示形式
      • 十进制数形式,例如:3.14 314.0 0.314
      • 科学记数法形式,如314e2 314E2 314E-2
    • 浮点类型float,double的数据不适合在不容许舍入误差的金融计算领域。如果需要进行不产生舍入误差的精确数字计算,需要使用BigDecimal类。
    • java.math包下面的两个有用的类:BigInteger和BigDecimal,这两个类可以处理任意长度的数值。BigInteger实现了任意精度的整数运算。BigDecimal实现了任意精度的浮点运算。
    • 不要使用浮点数进行比较!很多新人甚至很多理论不扎实的有工作经验的程序员也会犯这个错误!需要比较请使用BigDecimal类。
  • 字符型变量/常量

    • char 类型用来表示在Unicode编码表中的字符。Unicode编码被设计用来处理各种语言的文字,它占2个字节,可允许有65536个字符。

    • 转义字符

      img

    • String类,其实是字符序列(char sequence)。

  • 运算符(operator)

    img

  • 算术运算符

    • 取模运算:
      • 其操作数可以为浮点数,一般使用整数,结果是“余数”,“余数”符号和左边操作数相同,如:7%3=1,-7%3=-1,7%-3=1。
  • 赋值及其扩展赋值运算符

    img

  • 关系运算符

    img

  • 逻辑运算符

    img

    • 短路与和短路或采用短路的方式。从左到右计算,如果只通过运算符左边的操作数就能够确定该逻辑表达式的值,则不会继续计算运算符右边的操作数,提高效率。
  • 位运算符

    img

  • 运算符的优先级

    img

    • 逻辑与、逻辑或、逻辑非的优先级一定要熟悉!(逻辑非>逻辑与>逻辑或)。
  • 自动类型转换

    • 自动类型转换指的是容量小的数据类型可以自动转换为容量大的数据类型。实线表示无数据丢失的自动类型转换,而虚线表示在转换时可能会有精度的损失。

      1.png

    • 可以将整型常量直接赋值给byte、 short、 char等类型变量,而不需要进行强制类型转换,只要不超出其表数范围即可。

第3章 控制语句

  • 带标签的break和continue

    • “标签”是指后面跟一个冒号的标识符,例如:“label:”。对Java来说唯一用到标签的地方是在循环语句之前。而在循环之前设置标签的唯一理由是:我们希望在其中嵌套另一个循环,由于break和continue关键字通常只中断当前循环,但若随同标签使用,它们就会中断到存在标签的地方。

    • 在 “goto有害”论中,最有问题的就是标签,而非goto, 随着标签在一个程序里数量的增多,产生错误的机会也越来越多。 但Java标签不会造成这方面的问题,因为它们的活动场所已被限死,不可通过特别的方式到处传递程序的控制权。由此也引出了一个有趣的问题:通过限制语句的能力,反而能使一项语言特性更加有用。

    • 带标签break和continue:控制嵌套循环跳转(打印101-150之间所有的质数)

      public class Test18 {
          public static void main(String args[]) {
              outer: for (int i = 101; i < 150; i++) {
                  for (int j = 2; j < i / 2; j++) {
                      if (i % j == 0){
                          continue outer;
                      }
                  }
                  System.out.print(i + "  ");
              }
          }
      }
      
  • 方法

    • Java中进行方法调用中传递参数时,遵循值传递的原则(传递的都是数据的副本):
      • 基本类型传递的是该数据值的copy值。
      • 引用类型传递的是该对象引用的copy值,但指向的是同一个对象。
  • 方法的重载(overload)

    • 方法的重载是指一个类中可以定义多个方法名相同,但参数不同的方法。 调用时,会根据不同的参数自动匹配对应的方法。

    • 重载的方法,实际是完全不同的方法,只是名称相同而已!

    • 构成方法重载的条件:

      1. 不同的含义:形参类型形参个数形参顺序不同

      2. 只有返回值不同不构成方法的重载

      3. 只有形参的名称不同,不构成方法的重载

  • 递归结构

    • 递归是一种常见的解决问题的方法,即把问题逐渐简单化。递归的基本思想就是“自己调用自己”,一个使用递归技术的方法将会直接或者间接的调用自己。

    • 递归结构包括两个部分:

      1. 定义递归头。解答:什么时候不调用自身方法。如果没有头,将陷入死循环,也就是递归的结束条件。

      2. 递归体。解答:什么时候需要调用自身方法。

    • 递归的缺陷

      • 简单的程序是递归的优点之一。但是递归调用会占用大量的系统堆栈,内存耗用多,在递归调用层次多时速度要比循环慢的多,所以在使用递归时要慎重。
      • 任何能用递归解决的问题也能使用迭代解决。当递归方法可以更加自然地反映问题,并且易于理解和调试,并且不强调效率问题时,可以采用递归;
      • 在要求高性能的情况下尽量避免使用递归,递归调用既花时间又耗内存。

第4章 Java面向对象基础

1. 面向对象基础

  • 类:我们叫做class。
  • 对象:我们叫做Object,instance(实例)。以后我们说某个类的对象,某个类的实例。是一样的意思。

2. 面向对象的内存分析

  • Java虚拟机的内存可以分为三个区域:栈stack、堆heap、方法区method area。

  • 栈的特点如下:

    1. 栈描述的是方法执行的内存模型。每个方法被调用都会创建一个栈帧(存储局部变量、操作数、方法出口等)

    2. JVM为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数、局部变量等)

    3. 栈属于线程私有,不能实现线程间的共享!

    4. 栈的存储特性是“先进后出,后进先出”

      5. 栈是由系统自动分配,速度快!栈是一个连续的内存空间!

  • 堆的特点如下:

    1. 堆用于存储创建好的对象和数组(数组也是对象)

    2. JVM只有一个堆,被所有线程共享

    3. 堆是一个不连续的内存空间,分配灵活,速度慢!

  • 方法区(又叫静态区)特点如下:

    1. JVM只有一个方法区,被所有线程共享!

    2. 方法区实际也是堆,只是用于存储类、常量相关的信息!

    3. 用来存放程序中永远是不变或唯一的内容。(类信息【Class对象】、静态变量、字符串常量等)

    1.png

    neicunfenxi.png

3. 构造方法

  • 构造器也叫构造方法(constructor),用于对象的初始化。构造器是一个创建对象时被自动调用的特殊方法,目的是对象的初始化。构造器的名称应与类的名称一致。Java通过new关键字来调用构造器,从而返回该类的实例,是一种特殊的方法。

  • 要点:

    1. 通过new关键字调用!!

    2. 构造器虽然有返回值,但是不能定义返回值类型(返回值的类型肯定是本类),不能在构造器里使用return返回某个值。

    3. 如果我们没有定义构造器,则编译器会自动定义一个无参的构造函数。如果已定义则编译器不会自动添加!

    4. 构造器的方法名必须和类名一致!

  • 构造方法的重载

    • 构造方法也是方法,只不过有特殊的作用而已。与普通方法一样,构造方法也可以重载。
    • 构造方法的第一句总是super()
    • this表示创建好的对象

4. 垃圾回收机制(Garbage Collection)

  • Java引入了垃圾回收机制,令C++程序员最头疼的内存管理问题迎刃而解。Java程序员可以将更多的精力放到业务逻辑上而不是内存管理工作上,大大的提高了开发效率。

  • 内存管理

    • Java的内存管理很大程度指的就是对象的管理,其中包括对象空间的分配和释放。

    • 对象空间的分配:使用new关键字创建对象即可

    • 对象空间的释放:将对象赋值null即可。垃圾回收器将负责回收所有”不可达”对象的内存空间。

  • 垃圾回收过程

    任何一种垃圾回收算法一般要做两件基本事情:

    1. 发现无用的对象

    2. 回收无用对象占用的内存空间。

    垃圾回收机制保证可以将“无用的对象”进行回收。无用的对象指的就是没有任何变量引用该对象。Java的垃圾回收器通过相关算法发现无用对象,并进行清除和整理。

  • 垃圾回收相关算法

    1. 引用计数法

      堆中每个对象都有一个引用计数。被引用一次,计数加1. 被引用变量值变为null,则计数减1,直到计数为0,则表示变成无用对象。优点是算法简单,缺点是“循环引用的无用对象”无法别识别。

    2. 引用可达法(根搜索算法)

      程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

  • 通用的分代垃圾回收机制

    • 分代垃圾回收机制,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。我们将对象分为三种状态:年轻代、年老代、持久代。JVM将堆内存划分为 Eden、Survivor 和 Tenured/Old 空间。

      1. 年轻代

        所有新生成的对象首先都是放在Eden区。 年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,对应的是Minor GC,每次 Minor GC 会清理年轻代的内存,算法采用效率较高的复制算法,频繁的操作,但是会浪费内存空间。当“年轻代”区域存放满对象后,就将对象存放到年老代区域。

      2. 年老代

        在年轻代中经历了N(默认15)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。年老代对象越来越多,我们就需要启动Major GC和Full GC(全量回收),来一次大扫除,全面清理年轻代区域和年老代区域。

      3. 持久代

        用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。

    • 堆内存的划分细节

      1.png

      • Minor GC:

        用于清理年轻代区域。Eden区满了就会触发一次Minor GC。清理无用对象,将有用对象复制到“Survivor1”、“Survivor2”区中(这两个区,大小空间也相同,同一时刻Survivor1和Survivor2只有一个在用,一个为空)

      • Major GC:

        用于清理老年代区域。

      • Full GC:

        用于清理年轻代、年老代区域。 成本较高,会对系统性能产生影响。

    • 垃圾回收过程:

      1、新创建的对象,绝大多数都会存储在Eden中,

      2、当Eden满了(达到一定比例)不能创建新对象,则触发垃圾回收(GC),将无用对象清理掉,然后剩余对象复制到某个Survivor中,如S1,同时清空Eden区

      3、当Eden区再次满了,会将S1中的不能清空的对象存到另外一个Survivor中,如S2,同时将Eden区中的不能清空的对象,也复制到S1中,保证Eden和S1,均被清空。

      4、重复多次(默认15次)Survivor中没有被清理的对象,则会复制到老年代Old(Tenured)区中,

      5、当Old区满了,则会触发一个一次完整地垃圾回收(FullGC),之前新生代的垃圾回收称为(minorGC)

  • JVM调优和Full GC

    • 在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:

        1.年老代(Tenured)被写满

        2.持久代(Perm)被写满

        3.System.gc()被显式调用(程序建议GC启动,不是调用GC)

        4.上一次GC之后Heap的各域分配策略动态变化

  • 开发中容易造成内存泄露的操作

    • 创建大量无用对象:

      比如,我们在需要大量拼接字符串时,使用了String而不是StringBuilder。

    • 静态集合类的使用:

      像HashMap、Vector、List等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放。

    • 各种连接对象(IO流对象、数据库连接对象、网络连接对象)未关闭:

      IO流对象、数据库连接对象、网络连接对象等连接对象属于物理连接,和硬盘或者网络连接,不使用的时候一定要关闭。

    • 监听器的使用:

      释放对象时,没有删除相应的监听器。

  • 要点:

      1. 程序员无权调用垃圾回收器。

        2. 程序员可以调用System.gc(),该方法只是通知JVM,并不是运行垃圾回收器。尽量少用,会申请启动Full GC,成本高,影响系统性能。
    
              3. finalize方法,是Java提供给程序员用来释放对象或资源的方法,但是尽量少用。
    

5. this关键字

  • 对象创建的过程和this的本质

  • 构造方法是创建Java对象的重要途径,通过new关键字调用构造器时,构造器也确实返回该类的对象,但这个对象并不是完全由构造器负责创建。创建一个对象分为如下四步:

    1. 分配对象空间,并将对象成员变量初始化为0或空

    2. 执行属性值的显示初始化

    3. 执行构造方法

    4. 返回对象的地址给相关的变量

  • this的本质就是“创建好的对象的地址”! 由于在构造方法调用前,对象已经创建。因此,在构造方法中也可以使用this代表“当前对象” 。

  • this最常的用法:

    1. 在程序中产生二义性之处,应使用this来指明当前对象;普通方法中,this总是指向调用该方法的对象。构造方法中,this总是指向正要初始化的对象。

    2. 使用this关键字调用重载的构造方法,避免相同的初始化代码。但只能在构造方法中用,并且必须位于构造方法的第一句。

    3. this不能用于static方法中。

6. static 关键字

  • 在类中,用static声明的成员变量为静态成员变量,也称为类变量。 类变量的生命周期和类相同,在整个应用程序执行期间都有效。它有如下特点:

    1. 为该类的公用变量,属于类,被该类的所有实例共享,在类被载入时被显式初始化。

    2. 对于该类的所有对象来说,static成员变量只有一份。被该类的所有对象共享!

    3. 一般用“类名.类属性/方法”来调用。(也可以通过对象引用或类名(不需要实例化)访问静态成员。)

    4. 在static方法中不可直接访问非static的成员。

  • static修饰的成员变量和方法,从属于类。

  • 普通变量和方法从属于对象的。

2.png

7. 静态初始化块

  • 构造方法用于对象的初始化!静态初始化块,用于类的初始化操作!在静态初始化块中不能直接访问非static成员。

  • 静态初始化块执行顺序:

    1. 上溯到Object类,先执行Object的静态初始化块,再向下执行子类的静态初始化块,直到我们的类的静态初始化块为止。

    2. 构造方法执行顺序和上面顺序一样!!

8. 参数传值机制

  • Java中,方法中所有参数都是“值传递”,也就是“传递的是值的副本”。 也就是说,我们得到的是“原参数的复印件,而不是原件”。因此,复印件改变不会影响原件。
  • 基本数据类型参数的传值
    • 传递的是值的副本。 副本改变不会影响原件。
  • 引用类型参数的传值
    • 传递的是值的副本。但是引用类型指的是“对象的地址”。因此,副本和原参数都指向了同一个“地址”,改变“副本指向地址对象的值,也意味着原参数指向对象的值也发生了改变”。

9. 包

  • 包机制是Java中管理类的重要手段。 开发中,我们会遇到大量同名的类,通过包我们很容易对解决类重名的问题,也可以实现对类的有效管理。 包对于类,相当于文件夹对于文件的作用

  • 我们通过package实现对类的管理,package的使用有两个要点:

    1. 通常是类的第一句非注释性语句。

    2. 包名:域名倒着写即可,再加上模块名,便于内部管理类。

  • 注意事项:

    1. 写项目时都要加包,不要使用默认包。

    2. com.gao和com.gao.car,这两个包没有包含关系,是两个完全独立的包。只是逻辑上看起来后者是前者的一部分。

  • JDK中的主要包

    img

  • 导入类import

    • 如果我们要使用其他包的类,需要使用import导入,从而可以在本类中直接通过类名来调用,否则就需要书写类的完整包名和类名。import后,便于编写代码,提高可维护性。

    • 注意要点:

      1. Java会默认导入java.lang包下所有的类,因此这些类我们可以直接使用。

      2. 如果导入两个同名的类,只能用包名+类名来显示调用相关类。

  • 静态导入

    • 静态导入(static import)是在JDK1.5新增加的功能,其作用是用于导入指定类的静态属性,这样我们可以直接使用静态属性。

      package cn.sxt;
       //以下两种静态导入的方式二选一即可
      import static java.lang.Math.*;//导入Math类的所有静态属性
      import static java.lang.Math.PI;//导入Math类的PI属性
       
      public class Test2{
          public static void main(String [] args){
              System.out.println(PI);
              System.out.println(random());
          }
      }
      

第5章 Java面向对象进阶

1. 继承

  • 继承的实现

    • 继承让我们更加容易实现类的扩展。 比如,我们定义了人类,再定义Boy类就只需要扩展人类即可。实现了代码的重用,不用再重新发明轮子(don’t reinvent wheels)。

    • 从英文字面意思理解,extends的意思是“扩展”。子类是父类的扩展。现实世界中的继承无处不在。比如:

      图5-1 现实世界中的继承.png

  • 继承使用要点

    1. 父类也称作超类、基类、派生类等。

    2. Java中只有单继承,没有像C++那样的多继承。多继承会引起混乱,使得继承链过于复杂,系统难于维护。

    3. Java中类没有多继承,接口有多继承(??实现)。

    4. 子类继承父类,可以得到父类的全部属性和方法 (除了父类的构造方法),但不见得可以直接访问(比如,父类私有的属性和方法)。

    5. 如果定义一个类时,没有调用extends,则它的父类是:java.lang.Object。

    6. 可以使用ctrl+T方便的查看继承结构。

  • instanceof 运算符

    • instanceof是二元运算符,左边是对象,右边是类;当对象是右面类或子类所创建对象时,返回true;否则,返回false。
  • 方法的重写override

    • 子类通过重写父类的方法,可以用自身的行为替换父类的行为。方法的重写是实现多态的必要条件

    • 方法的重写需要符合下面的三个要点:

      ​ 1.“==”: 方法名、形参列表相同。

      ​ 2.“≤”:返回值类型和声明异常类型,子类小于等于父类。

      ​ 3.“≥”: 访问权限,子类大于等于父类。

2. Object类

  • Object类是所有Java类的根基类,也就意味着所有的Java对象都拥有Object类的属性和方法。如果在类的声明中未使用extends关键字指明其父类,则默认继承Object类。

  • toString方法

    • Object类中定义有public String toString()方法,其返回值是 String 类型。

    • Object类中toString方法的源码为:

      public String toString() {
          return getClass().getName() + "@" + Integer.toHexString(hashCode());
      }
      
    • 根据如上源码得知,默认会返回“类名+@+16进制的hashcode”。在打印输出或者用字符串连接对象时,会自动调用该对象的toString()方法。

  • ==和equals方法

    • “==”代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表示地址相等即是同一个对象。
    • Object类中定义有:public boolean equals(Object obj)方法,提供定义“对象内容相等”的逻辑。比如,我们在公安系统中认为id相同的人就是同一个人、学籍系统中认为学号相同的人就是同一个人。
    • Object 的 equals 方法默认就是比较两个对象的hashcode,是同一个对象的引用时返回 true 否则返回 false。但是,我们可以根据我们自己的要求重写equals方法。
    • JDK提供的一些类,如String、Date、包装类等,重写了Object的equals方法,调用这些类的equals方法, x.equals (y) ,当x和y所引用的对象是同一类对象且属性内容相等时(并不一定是相同对象),返回 true 否则返回 false。

3. super关键字

  • super是直接父类对象的引用。可以通过super来访问父类中被子类覆盖的方法或属性。

  • 使用super调用普通方法,语句没有位置限制,可以在子类中随便调用。

  • 若是构造方法的第一行代码没有显式的调用super(...)或者this(...);那么Java默认都会调用super(),含义是调用父类的无参数构造方法。这里的super()可以省略。

  • 继承树追溯

  • 属性/方法查找顺序:(比如:查找变量h)

    1. 查找当前类中有没有属性h

    2. 依次上溯每个父类,查看每个父类中是否有h,直到Object

    3. 如果没找到,则出现编译错误。

    4. 上面步骤,只要找到h变量,则这个过程终止。

  • 构造方法调用顺序:

    • 构造方法第一句总是:super(…)来调用父类对应的构造方法。所以,流程就是:先向上追溯到Object,然后再依次向下执行类的初始化块和构造方法,直到当前子类为止。
      • 注:静态初始化块调用顺序,与构造方法调用顺序一样,不再重复。

4. 封装

  • 需要让用户知道的才暴露出来,不需要让用户知道的全部隐藏起来,这就是封装。说的专业一点,封装就是把对象的属性和操作结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。

  • 我们程序设计要追求“高内聚,低耦合”。 高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合是仅暴露少量的方法给外部使用,尽量方便外部调用。

  • 编程中封装的具体优点:

    1. 提高代码的安全性。

    2. 提高代码的复用性。

    3. “高内聚”:封装细节,便于修改内部代码,提高可维护性。

    4. “低耦合”:简化外部调用,便于调用者使用,便于扩展和协作。

  • Java是使用“访问控制符”来控制哪些细节需要封装,哪些细节需要暴露的。 Java中4种“访问控制符”分别为private、default、protected、public,它们说明了面向对象的封装性,所以我们要利用它们尽可能的让访问权限降到最低,从而提高安全性。

    图1.png

    1. private 表示私有,只有自己类能访问

    2. default表示没有修饰符修饰,只有同一个包的类能访问

    3. protected表示可以被同一个包的类以及其他包中的子类访问

    4. public表示可以被该项目的所有包中的所有类访问

  • 类的属性的处理:

    1. 一般使用private访问权限。
  1. 提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的,以提供对属性的赋值与读取操作(注意:boolean变量的get方法是is开头!)。
  2. 一些只用于本类的辅助性方法可以用private修饰,希望其他类调用的方法用public修饰。

5. 多态(polymorphism)

  • 多态指的是同一个方法调用,由于对象不同可能会有不同的行为。

  • 多态的要点:

    1. 多态是方法的多态,不是属性的多态(多态与属性无关)。

    2. 多态的存在要有3个必要条件:继承方法重写父类引用指向子类对象

    3. 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。

    4. 多态最为多见的一种用法,即父类引用做方法的形参,实参可以是任意的子类对象,可以通过不同的子类对象实现不同的行为方式。

    5. 由此,我们可以看出多态的主要优势是提高了代码的可扩展性,符合开闭原则。但是多态也有弊端,就是无法调用子类特有的功能。

  • 对象的转型(casting)

    • 父类引用指向子类对象,我们称这个过程为向上转型,属于自动类型转换。
    • 向上转型后的父类引用变量只能调用它编译类型的方法,不能调用它运行时类型的方法。这时,我们就需要进行类型的强制转换,我们称之为向下转型!
    • 在向下转型过程中,必须将引用变量转成真实的子类类型(运行时类型)否则会出现类型转换异常ClassCastException。

6. final关键字

  • 修饰变量: 被final修饰的变量不可改变。一旦赋了初值,就不能被重新赋值。
  • 修饰方法:该方法不可被子类重写。但是可以被重载!
  • 修饰类: 修饰的类不能被继承。比如:Math、String等。(重写就是同一个方法,子类和父类实现的功能不一样,重载就是传参类型啊个数啊,等等,不一样的话,虽然名字相同,但也是两个方法。)

7. 抽象方法和抽象类

  • 抽象方法:使用abstract修饰的方法,没有方法体,只有声明。定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体的实现。

  • 抽象类:包含抽象方法的类就是抽象类。通过abstract方法定义规范,然后要求子类必须定义具体实现。通过抽象类,我们就可以做到严格限制子类的设计,使子类之间更加通用。

  • 抽象类的使用要点:

    1. 有抽象方法的类只能定义成抽象类

    2. 抽象类不能实例化,即不能用new来实例化抽象类。

    3. 抽象类可以包含属性、方法、构造方法。但是构造方法不能用来new实例,只能用来被子类调用。

    4. 抽象类只能用来被继承。

    5. 抽象方法必须被子类实现。

8. 接口

  • 为什么需要接口?接口和抽象类的区别?

    • 接口就是比“抽象类”还“抽象”的“抽象类”,可以更加规范的对子类进行约束。全面地专业地实现了:规范和具体实现的分离。

    • 抽象类还提供某些具体实现,接口不提供任何实现,接口中所有方法都是抽象方法。接口是完全面向规范的,规定了一批类具有的公共方法规范。

    • 从接口的实现者角度看,接口定义了可以向外部提供的服务。

    • 从接口的调用者角度看,接口定义了实现者能提供哪些服务。

    • 接口是两个模块之间通信的标准,通信的规范。如果能把你要设计的模块之间的接口定义好,就相当于完成了系统的设计大纲,剩下的就是添砖加瓦的具体实现了。大家在工作以后,做系统时往往就是使用“面向接口”的思想来设计系统。

    • 接口和实现类不是父子关系,是实现规则的关系。比如:我定义一个接口Runnable,Car实现它就能在地上跑,Train实现它也能在地上跑,飞机实现它也能在地上跑。就是说,如果它是交通工具,就一定能跑,但是一定要实现Runnable接口。

  • 接口的本质探讨

    • 接口就是规范,定义的是一组规则,体现了现实世界中“如果你是…则必须能…”的思想。如果你是天使,则必须能飞。如果你是汽车,则必须能跑。如果你是好人,则必须能干掉坏人;如果你是坏人,则必须欺负好人。

    • 接口的本质是契约,就像我们人间的法律一样。制定好后大家都遵守。

    • 面向对象的精髓,是对对象的抽象,最能体现这一点的就是接口。为什么我们讨论设计模式都只针对具备了抽象能力的语言(比如C++、Java、C#等),就是因为设计模式所研究的,实际上就是如何合理的去抽象。

  • 区别

    1. 普通类:具体实现

    2. 抽象类:具体实现,规范(抽象方法)

    3. 接口:规范!

  • 声明格式:

    [访问修饰符]  interface 接口名   [extends  父接口1,父接口2…]  {
    常量定义;  
    方法定义;
    }
    
  • 定义接口的详细说明:

    1. 访问修饰符:只能是public或默认。

    2. 接口名:和类名采用相同命名机制。

    3. extends:接口可以多继承。

    4. 常量:接口中的属性只能是常量,总是:public static final 修饰。不写也是。

    5. 方法:接口中的方法只能是:public abstract。 省略的话,也是public abstract。

  • 要点

    1. 子类通过implements来实现接口中的规范。

    2. 接口不能创建实例,但是可用于声明引用变量类型

    3. 一个类实现了接口,必须实现接口中所有的方法,并且这些方法只能是public的。

    4. JDK1.7之前,接口中只能包含静态常量、抽象方法,不能有普通属性、构造方法、普通方法。

    5. JDK1.8后,接口中包含普通的静态方法。

  • 接口的多继承

    • 接口完全支持多继承。和类的继承类似,子接口扩展某个父接口,将会获得父接口中所定义的一切。
    • 接口可以多继承(extends),类只能单继承,但可以实现(implements)多个接口。
  • 通过面向接口编程,而不是面向实现类编程,可以大大降低程序模块间的耦合性,提高整个系统的可扩展性和和可维护性。

  • 面向接口编程的概念比接口本身的概念要大得多。设计阶段相对比较困难,在你没有写实现时就要想好接口,接口一变就乱套了,所以设计要比实现难!

9. 内部类

  • 一般情况,我们把类定义成独立的单元。有些情况下,我们把一个类放在另一个类的内部定义,称为内部类(innerclasses)

  • 内部类可以使用public、default、protected 、private以及static修饰。而外部顶级类(我们以前接触的类)只能使用public和default修饰。

  • 注意:内部类只是一个编译时概念,一旦我们编译成功,就会成为完全不同的两个类。对于一个名为Outer的外部类和其内部定义的名为Inner的内部类。编译完成后会出现Outer.classOuter$Inner.class两个类的字节码文件。所以内部类是相对独立的一种存在,其成员变量/方法名可以和外部类的相同。

  • 内部类的作用:

    1. 内部类提供了更好的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问。

    2. 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。 但外部类不能访问内部类的内部属性。

    3. 接口只是解决了多重继承的部分问题,而内部类使得多重继承的解决方案变得更加完整

  • 内部类的使用场合:

    1. 由于内部类提供了更好的封装特性,并且可以很方便的访问外部类的属性。所以,在只为外部类提供服务的情况下可以优先考虑使用内部类。

    2. 使用内部类间接实现多继承:每个内部类都能独立地继承一个类或者实现某些接口,所以无论外部类是否已经继承了某个类或者实现了某些接口,对于内部类没有任何影响。

  • 内部类的分类

    • 在Java中内部类主要分为成员内部类(非静态内部类、静态内部类)、匿名内部类、局部内部类。

    • 成员内部类(可以使用private、default、protected、public任意进行修饰。 类文件:外部类$内部类.class)

      • 非静态内部类(外部类里使用非静态内部类和平时使用其他类没什么不同)

        • 非静态内部类必须寄存在一个外部类对象里。因此,如果有一个非静态内部类对象那么一定存在对应的外部类对象。非静态内部类对象单独属于外部类的某个对象。

        • 非静态内部类可以直接访问外部类的成员,但是外部类不能直接访问非静态内部类成员。

        • 非静态内部类不能有静态方法、静态属性和静态初始化块。

        • 外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例。

        • 成员变量访问要点:

          1. 内部类里方法的局部变量:变量名。

          2. 内部类属性:this.变量名。

          3. 外部类属性:外部类名.this.变量名。

          class Outer {
              private int age = 10;
              class Inner {
                  int age = 20;
                  public void show() {
                      int age = 30;
                      System.out.println("内部类方法里的局部变量age:" + age);// 30
                      System.out.println("内部类的成员变量age:" + this.age);// 20
                      System.out.println("外部类的成员变量age:" + Outer.this.age);// 10
                  }
              }
          }
          
        • 内部类的访问:

          1. 外部类中定义内部类

            new Inner()
            
          2. 外部类以外的地方使用非静态内部类

            Outer.Inner  varname = new Outer().new Inner()
            
      • 静态内部类

        • 定义方式

          static  class   ClassName {
          //类体
          }
          
        • 使用要点

          1. 当一个静态内部类对象存在,并不一定存在对应的外部类对象。 因此,静态内部类的实例方法不能直接访问外部类的实例方法。

          2. 静态内部类看做外部类的一个静态成员。 因此,外部类的方法中可以通过:“静态内部类.名字”的方式访问静态内部类的静态成员,通过 new 静态内部类()访问静态内部类的实例。

          class Outer{
              //相当于外部类的一个静态成员
              static class Inner{
              }
          }
          public class TestStaticInnerClass {
              public static void main(String[] args) {
                  //通过 new 外部类名.内部类名() 来创建内部类对象
                  Outer.Inner inner =new Outer.Inner();
              }
          }
          
    • 匿名内部类

      • 适合那种只需要使用一次的类。比如:键盘监听操作等等。

        new  父类构造器(实参类表) \实现接口 () {
                   //匿名内部类类体!
        }
        
        this.addWindowListener(new WindowAdapter(){
                @Override
                public void windowClosing(WindowEvent e) {
                    System.exit(0);
                }
            }
        );
        this.addKeyListener(new KeyAdapter(){
                @Override
                public void keyPressed(KeyEvent e) {
                    myTank.keyPressed(e);
                }      
                @Override
                public void keyReleased(KeyEvent e) {
                    myTank.keyReleased(e);
                }
            }
        );
        
      • 注意

        1. 匿名内部类没有访问修饰符。

        2. 匿名内部类没有构造方法。因为它连名字都没有那又何来构造方法呢。

    • 局部内部类

      • 还有一种内部类,它是定义在方法内部的,作用域只限于本方法,称为局部内部类。

      • 局部内部类的的使用主要是用来解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类。局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法中被使用,出了该方法就会失效。

      • 局部内部类在实际开发中应用很少。

        public class Test2 {
            public void show() {
                //作用域仅限于该方法
                class Inner {
                    public void fun() {
                        System.out.println("helloworld");
                    }
                }
                new Inner().fun();
            }
            public static void main(String[] args) {
                new Test2().show();
            }
        }
        

10. String

  • String基础

    1. String类又称作不可变字符序列。

    2. String位于java.lang包中,Java程序默认导入java.lang包下的所有类。

    3. Java字符串就是Unicode字符序列,例如字符串“Java”就是4个Unicode字符’J’、’a’、’v’、’a’组成的。

    4. Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义的类String,每个用双引号括起来的字符串都是String类的一个实例。

    5. Java允许使用符号"+"把两个字符串连接起来。

  • String类和常量池

    • 在Java的内存分析中,我们会经常听到关于“常量池”的描述,实际上常量池也分了以下三种:

      1. 全局字符串常量池(String Pool):全局字符串常量池中存放的内容是在类加载完成后存到String Pool中的,在每个VM中只有一份,存放的是字符串常量的引用值(在堆中生成字符串对象实例)。

      2. class文件常量池(Class Constant Pool):class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量(文本字符串、final常量等)和符号引用。

      3. 运行时常量池(Runtime Constant Pool):运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

      String str1 = "abc";
      String str2 = new String("def");
      String str3 = "abc";
      String str4 = str2.intern();
      String str5 = "def";
      System.out.println(str1 == str3);// true
      System.out.println(str2 == str4);// false
      System.out.println(str4 == str5);// true
      
    • 示例首先经过编译之后,在该类的class常量池中存放一些符号引用,然后类加载之后,将class常量池中存放的符号引用转存到运行时常量池中,然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中str1所指向的“abc”实例对象),然后将这个对象的引用存到全局String Pool中,也就是String Pool中,最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用,那么就直接查询String Pool,保证String Pool里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。

    • 回到示例程序,现在就很容易解释整个程序的内存分配过程了,首先,在堆中会有一个“abc”实例,全局String Pool中存放着“abc”的一个引用值,然后在运行第二句的时候会生成两个实例,一个是“def”的实例对象,并且String Pool中存储一个“def”的引用值,还有一个是new出来的一个“def”的实例对象,与上面那个是不同的实例,当在解析str3的时候查找String Pool,里面有“abc”的全局驻留字符串引用,所以str3的引用地址与之前的那个已存在的相同,str4是在运行的时候调用intern()函数,返回String Pool中“def”的引用值,如果没有就将str2的引用值添加进去,在这里,String Pool中已经有了“def”的引用值了,所以返回上面在new str2的时候添加到String Pool中的 “def”引用值,最后str5在解析的时候就也是指向存在于String Pool中的“def”的引用值,那么这样一分析之后,结果就容易理解了。

  • String类的常用方法

    图2.png

11. 补充内容

  • 开闭原则
    • 开闭原则(Open-Closed Principle)就是让设计的系统对扩展开放,对修改封闭。
    • 对扩展开放:就是指,应对需求变化要灵活。 要增加新功能时,不需要修改已有的代码,增加新代码即可。
    • 对修改关闭:就是指,核心部分经过精心设计后,不再因为需求变化而改变。
  • 模板方法模式和回调机制
    • 板方法模式很常用,其目的是在一个方法中定义一个算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。在标准的模板方法模式实现中,主要是使用继承的方式,来让父类在运行期间可以调用到子类的方法。
    • 其实在Java开发中,还有另外一个方法可以实现同样的功能,那就是Java回调技术。回调是一种双向的调用模式,也就是说,被调用的接口被调用时也会调用对方的接口,简单点说明就是:A类中调用B类中的C方法,然后B类中的C方法中反过来调用A类中的D方法,那么D这个方法就叫回调方法。
    • 通过回调在接口中定义的方法,调用到具体的实现类中的方法,其本质是利用Java的动态绑定技术,在这种实现中,可以不把实现类写成单独的类,而使用内部类或匿名内部类来实现回调方法。
    • 理解的话参考文献:Java设计模式——模板方法模式
  • 组合模式
    • 组合模式是将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

第6章 异常机制

1. 异常

  • 异常机制本质:就是当程序出现错误,程序安全退出的机制。

    try {
        copyFile("d:/a.txt","e:/a.txt");
    } catch (Exception e) {
        e.printStackTrace();
    }
    
  • 异常(Exception)的概念

    • 异常指程序运行过程中出现的非正常现象,例如用户输入错误、除数为零、需要处理的文件不存在、数组下标越界等。

    • 在Java的异常处理机制中,引进了很多用来描述和处理异常的类,称为异常类。异常类定义中包含了该类异常的信息和对异常进行处理的方法。

    • 所谓异常处理,就是指程序在出现问题时依然可以正确的执行完。

    • Java是采用面向对象的方式来处理异常的。处理过程:

      1. 抛出异常:在执行一个方法时,如果发生异常,则这个方法生成代表该异常的一个对象,停止当前执行路径,并把异常对象提交给JRE。

      2. 捕获异常:JRE得到该异常后,寻找相应的代码来处理该异常。JRE在方法的调用栈中查找,从生成异常的方法开始回溯,直到找到相应的异常处理代码为止。

  • 异常分类

    Java对异常进行了分类,不同类型的异常分别用不同的Java类表示,所有异常的根类为java.lang.Throwable,Throwable下面又派生了两个子类:Error和Exception。Java异常类的层次结构如图所示。

    图6-2 Java异常类层次结构图.png

    • Error

      • Error是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
      • Error表明系统JVM已经处于不可恢复的崩溃状态中。我们不需要管它。
    • Exception

      • Exception是程序本身能够处理的异常,如:空指针异常(NullPointerException)、数组下标越界异常(ArrayIndexOutOfBoundsException)、类型转换异常(ClassCastException)、算术异常(ArithmeticException)等。

      • Exception类是所有异常类的父类,其子类对应了各种各样可能出现的异常事件。 通常Java的异常可分为:

        1. RuntimeException 运行时异常

          • 派生于RuntimeException的异常,如被 0 除、数组下标越界、空指针等,其产生比较频繁,处理麻烦,如果显式的声明或捕获将会对程序可读性和运行效率影响很大。 因此由系统自动检测并将它们交给缺省的异常处理程序(用户可不必对其处理)。

          • 这类异常通常是由编程错误导致的,所以在编写程序时,并不要求必须使用异常处理机制来处理这类异常,经常需要通过增加“逻辑处理来避免这些异常”。

          • 在引用数据类型转换时,有可能发生类型转换异常(ClassCastException)。

          • 当程序访问一个数组的某个元素时,如果这个元素的索引超出了0~数组长度-1这个范围,则会出现数组下标越界异常(ArrayIndexOutOfBoundsException)。

          • 在使用包装类将字符串转换成基本数据类型时,如果字符串的格式不正确,则会出现数字格式异常(NumberFormatException)。

          • 注意事项

            1. 在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。

            2. 运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

        2. CheckedException 已检查异常

          • 所有不是RuntimeException的异常,统称为Checked Exception,又被称为“已检查异常”,如IOException、SQLException等以及用户自定义的Exception异常。 这类异常在编译时就必须做出处理,否则无法通过编译。
          • 异常的处理方式有两种:使用“try/catch”捕获异常、使用“throws”声明异常。

2. 处理异常

  • 异常的处理方式之一:捕获异常

    • 捕获异常是通过3个关键词来实现的:try-catch-finally。用try来执行一段程序,如果出现异常,系统抛出一个异常,可以通过它的类型来捕捉(catch)并处理它,最后一步是通过finally语句为异常处理提供一个统一的出口,finally所指定的代码都要被执行(catch语句可有多条;finally语句最多只能有一条,根据自己的需要可有可无)。图6-10 异常处理.png

    • 1. try:

      • try语句指定了一段代码,该段代码就是异常捕获并处理的范围。在执行过程中,当任意一条语句产生异常时,就会跳过该条语句中后面的代码。代码中可能会产生并抛出一种或几种类型的异常对象,它后面的catch语句要分别对这些异常做相应的处理。

      • 一个try语句必须带有至少一个catch语句块或一个finally语句块 。

      注意事项

      • 当异常处理的代码执行结束以后,不会回到try语句去执行尚未执行的代码
    • 2. catch:

      • 每个try语句块可以伴随一个或多个catch语句,用于处理可能产生的不同类型的异常对象。

      • 常用方法,这些方法均继承自Throwable类 。

      • toString()方法,显示异常的类名和产生异常的原因

      • getMessage()方法,只显示产生异常的原因,但不显示类名。

      • printStackTrace()方法,用来跟踪异常事件发生时堆栈的内容。

      • catch捕获异常时的捕获顺序:如果异常类之间有继承关系,在顺序安排上需注意。越是顶层的类,越放在下面,再不然就直接把多余的catch省略掉。也就是先捕获子类异常再捕获父类异常。

    • 3. finally:

      • 有些语句,不管是否发生了异常,都必须要执行,那么就可以把这样的语句放到finally语句块中。

      • 通常在finally中关闭程序块已打开的资源,比如:关闭文件流、释放数据库连接等。

    • try-catch-finally语句块的执行过程:程序首先执行可能发生异常的try语句块。如果try语句没有出现异常则执行完后跳至finally语句块执行;如果try语句出现异常,则中断执行并根据发生的异常类型跳至相应的catch语句块执行处理。catch语句块可以有多个,分别捕获不同类型的异常。catch语句块执行完后程序会继续执行finally语句块。finally语句是可选的,如果有的话,则不管是否发生异常,finally语句都会被执行。

    • 注意事项

      1. 即使try和catch块中存在return语句,finally语句也会执行。是在执行完finally语句后再通过return退出。

      2. finally语句块只有一种情况是不会执行的,那就是在执行finally之前遇到了System.exit(0)结束程序运行。

  • 异常的处理方式之二:声明异常(throws子句)

    • 当CheckedException产生时,不一定立刻处理它,可以再把异常throws出去。
    • 在方法中使用try-catch-finally是由这个方法来处理异常。但是在一些情况下,当前方法并不需要处理发生的异常,而是向上传递给调用它的方法处理。
    • 如果一个方法中可能产生某种异常,但是并不能确定如何处理这种异常,则应根据异常规范在方法的首部声明该方法可能抛出的异常。
    • 如果一个方法抛出多个已检查异常,就必须在方法的首部列出所有的异常,之间以逗号隔开。
    • 注意事项:方法重写中声明异常原则:子类重写父类方法时,如果父类方法有声明异常,那么子类声明的异常范围不能超过父类声明的范围。

3. 自定义异常

  1. 在程序中,可能会遇到JDK提供的任何标准异常类都无法充分描述清楚我们想要表达的问题,这种情况下可以创建自己的异常类,即自定义异常类。
  2. 自定义异常类只需从Exception类或者它的子类派生一个子类即可。
  3. 自定义异常类如果继承Exception类,则为受检查异常,必须对其进行处理;如果不想处理,可以让自定义异常类继承运行时异常RuntimeException类。
  4. 习惯上,自定义异常类应该包含2个构造器:一个是默认的构造器,另一个是带有详细信息的构造器。
  5. 使用异常机制的建议
    1. 要避免使用异常处理代替错误处理,这样会降低程序的清晰性,并且效率低下。
    2. 处理异常不可以代替简单测试---只在异常情况下使用异常机制。
    3. 不要进行小粒度的异常处理---应该将整个任务包装在一个try语句块中。
    4. 异常往往在高层处理 。

第7章 数组

1. 数组概述和特点

  • 数组的定义

    数组是相同类型数据的有序集合。数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成。其中,每一个数据称作一个元素,每个元素可以通过一个索引(下标)来访问它们。数组的三个基本特点:

    1. 长度是确定的。数组一旦被创建,它的大小就是不可以改变的。

    2. 其元素必须是相同类型,不允许出现混合类型。

    3. 数组类型可以是任何数据类型,包括基本类型和引用类型。

  • 数组变量属引用类型,数组也可以看成是对象,数组中的每个元素相当于该对象的成员变量。数组本身就是对象,Java中对象是在堆中的,因此数组无论保存原始类型还是其他对象类型,数组对象本身是在堆中存储的。

  • 数组声明

    • 数组的声明方式有两种(以一维数组为例)

      type[]   arr_name; //(推荐使用这种方式)
      type    arr_name[];
      
    • 注意事项

      1. 声明的时候并没有实例化任何对象,只有在实例化数组对象时,JVM才分配空间,这时才与长度有关。

      2. 声明一个数组的时候并没有数组真正被创建。

      3. 构造一个数组,必须指定长度。

    图7-1 基本类型数组内存分配图.png

  • 初始化

    ​ 数组的初始化方式总共有三种:静态初始化、动态初始化、默认初始化。下面针对这三种方式分别讲解。

    1. 静态初始化

    ​ 除了用new关键字来产生数组以外,还可以直接在定义数组的同时就为数组元素分配空间并赋值。

    int[] a = { 1, 2, 3 };// 静态初始化基本类型数组;
    Man[] mans = { new Man(1, 1), new Man(2, 2) };// 静态初始化引用类型数组;
    

    2.动态初始化

    ​ 数组定义与为数组元素分配空间并赋值的操作分开进行。

    int[] a1 = new int[2];//动态初始化数组,先分配空间;
    a1[0]=1;//给数组元素赋值;
    a1[1]=2;//给数组元素赋值;
    

    3.数组的默认初始化

    ​ 数组是引用类型,它的元素相当于类的实例变量,因此数组一经分配空间,其中的每个元素也被按照实例变量同样的方式被隐式初始化。

    int a2[] = new int[2]; // 默认值:0,0
    boolean[] b = new boolean[2]; // 默认值:false,false
    String[] s = new String[2]; // 默认值:null, null
    

2. 数组的遍历

  • 数组元素下标的合法区间:[0, length-1]。我们可以通过下标来遍历数组中的元素,遍历时可以读取元素的值或者修改元素的值。使用循环遍历初始化和读取数组。

    public class Test {
        public static void main(String[] args) {
            int[] a = new int[4];
            //初始化数组元素的值
            for(int i=0;i<a.length;i++){
                a[i] = 100*i;
            }
            //读取元素的值
            for(int i=0;i<a.length;i++){
                System.out.println(a[i]);
            }
        }
    }
    
  • for-each循环:增强for循环for-each是JDK1.5新增加的功能,专门用于读取数组或集合中所有的元素,即对数组进行遍历。

    public class Test {
        public static void main(String[] args) {
            String[] ss = { "aa", "bbb", "ccc", "ddd" };
            for (String temp : ss) {
                System.out.println(temp);
            }
        }
    }
    
  • 注意事项

    1. for-each增强for循环在遍历数组过程中不能修改数组中某元素的值。

    2. for-each仅适用于遍历,不涉及有关索引(下标)的操作。

3. 数组的拷贝

  • System类里也包含了一个static void arraycopy(object src,int srcpos,object dest, int destpos,int length)方法,该方法可以将src数组里的元素值赋给dest数组的元素,其中srcpos指定从src数组的第几个元素开始赋值,length参数指定将src数组的多少个元素赋给dest数组的元素。

    public class testCopy {
        public static void main(String[] args) {
            String[] s1 = {"aa", "bb", "cc", "dd", "ee"};
            String[] s2 = new String[10];
            System.arraycopy(s1, 2, s2, 6, 3);
    
            for(int i=0; i<s2.length; i++){
                System.out.println(i+"--"+s2[i]);
            }
    
            System.out.println("########");
            removeElement(s1, 2);
            System.out.println("########");
            extendRange(s1);
        }
    
        // 删除数组中指定索引位置的元素,并将原数组返回
        private static String[] removeElement(String[] s, int index){
            System.arraycopy(s, index+1, s, index, s.length-index-1);
            s[s.length-1] = null;
    
            for (String m:
                 s) {
                System.out.println(m);
            }
            return s;
        }
    
        // 数组的扩容(本质上时:先定义一个更大的数组,然后将原数组内容原封不动拷贝到新数组中)
        private static String[] extendRange(String[] s){
            String[] s2 = new String[s.length + 10];
            System.arraycopy(s, 0, s2, 0, s.length);
    
            for(String x: s2){
                System.out.println(x);
            }
            return s2;
        }
    }
    

4. java.util.Arrays类

  • JDK提供的java.util.Arrays类,包含了常用的数组操作,方便我们日常开发。Arrays类包含了:排序、查找、填充、打印内容等常见的操作。

    import java.util.Arrays;
    
    public class TestArrays {
        public static void main(String[] args) {
            int[] a = {100, 20, 30, 5, 150, 80, 200};
            System.out.println(a);
            System.out.println(Arrays.toString(a));
            Arrays.sort(a);
            System.out.println(Arrays.toString(a));
    
            // 二分法查找
            System.out.println(Arrays.binarySearch(a, 30));
    
            // 数组填充,前闭后开
            Arrays.fill(a, 2, 4, 800);
            System.out.println(Arrays.toString(a));
        }
    }
    

5. 多维数组

  • 多维数组可以看成以数组为元素的数组。可以有二维、三维、甚至更多维数组,但是实际开发中用的非常少。最多到二维数组(学习容器后,我们一般使用容器,二维数组用的都很少)。

    • 二维数组的声明

      public class Test {
          public static void main(String[] args) {
              // Java中多维数组的声明和初始化应按从低维到高维的顺序进行
              int[][] a = new int[3][];
              a[0] = new int[2];
              a[1] = new int[4];
              a[2] = new int[3];
              // int a1[][]=new int[][4];//非法
          }
      }
      
    • 二维数组的静态初始化

      public class Test {
          public static void main(String[] args) {
              int[][] a = { { 1, 2, 3 }, { 3, 4 }, { 3, 5, 6, 7 } };
              System.out.println(a[2][3]);
          }
      }
      
    • 二维数组的动态初始化

      import java.util.Arrays;
      public class Test {
          public static void main(String[] args) {
              int[][] a = new int[3][];
              // a[0] = {1,2,5}; //错误,没有声明类型就初始化
              a[0] = new int[] { 1, 2 };
              a[1] = new int[] { 2, 2 };
              a[2] = new int[] { 2, 2, 3, 4 };
              System.out.println(a[2][3]);
              System.out.println(Arrays.toString(a[0]));
              System.out.println(Arrays.toString(a[1]));
              System.out.println(Arrays.toString(a[2]));
          }
      }
      
    • 获取数组长度

      //获取的二维数组第一维数组的长度。
      System.out.println(a.length);
      //获取第二维第一个数组长度。
      System.out.println(a[0].length);
      
    • 数组存储表格数据

      import java.util.Arrays;
      public class Test {
          public static void main(String[] args) {
              Object[] a1 = {1001,"高淇",18,"讲师","2006-2-14"};
              Object[] a2 = {1002,"高小七",19,"助教","2007-10-10"};
              Object[] a3 = {1003,"高小琴",20,"班主任","2008-5-5"};
              Object[][]  emps = new Object[3][];
              emps[0] = a1;
              emps[1] = a2;
              emps[2] = a3;
              System.out.println(Arrays.toString(emps[0]));
              System.out.println(Arrays.toString(emps[1]));
              System.out.println(Arrays.toString(emps[2]));  
          }
      }
      
      • 注意事项:此处基本数据类型”1001”,本质不是Object对象。JAVA编译器会自动把基本数据类型“自动装箱”成包装类对象。大家在下一章学了包装类后就懂了。

6. 冒泡排序和二分法查找

  • 冒泡排序

    import java.util.Arrays;
    
    public class TestBubbleSort {
        public static void main(String[] args) {
            int[] values = {3, 1, 6, 2, 9, 0, 7, 4, 5, 8};
            bubble(values);
            bubble_improve(values);
        }
    
        public static void bubble(int[] values){
            /*冒泡排序*/
            int temp = 0;
    
            // 遍历前n-1个数
            for (int i = 0; i < values.length-1; i++) {
                // 每次循环都把最大值放到后面,所以后面的就不必在比较了
                for (int j = 0; j < values.length-1-i; j++) {
                    // 如果前一个值大于后一个值,则交换位置
                    if(values[j]>values[j+1]){
                        temp = values[j];
                        values[j] = values[j+1];
                        values[j+1] = temp;
                    }
                }
            }
            System.out.println(Arrays.toString(values));
        }
    
        public static void bubble_improve(int[] values){
            /* 改良冒泡排序 */
            /* 也就是当一次外循环没有发生交换的时候,那么停止 */
            int temp = 0;
    
            // 遍历前n-1个数
            for (int i = 0; i < values.length-1; i++) {
                // 每次循环都把最大值放到后面,所以后面的就不必在比较了
                boolean flag = true;
                for (int j = 0; j < values.length-1-i; j++) {
                    // 如果前一个值大于后一个值,则交换位置
                    if(values[j]>values[j+1]){
                        temp = values[j];
                        values[j] = values[j+1];
                        values[j+1] = temp;
    
                        flag = false;
                    }
                }
    
                if(flag){
                    break;
                }
            }
            System.out.println(Arrays.toString(values));
        }
    }
    
    • 冒泡排序算法的运作如下:

      1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。

      2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。

      3. 针对所有的元素重复以上的步骤,除了最后一个。

      4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

    • 优化冒泡排序:

      1. 整个数列分成两部分:前面是无序数列,后面是有序数列。

      2. 初始状态下,整个数列都是无序的,有序数列是空。

      3. 每一趟循环可以让无序数列中最大数排到最后,(也就是说有序数列的元素个数增加1),也就是不用再去顾及有序序列。

      4. 每一趟循环都从数列的第一个元素开始进行比较,依次比较相邻的两个元素,比较到无序数列的末尾即可(而不是数列的末尾);如果前一个大于后一个,交换。

      5. 判断每一趟是否发生了数组元素的交换,如果没有发生,则说明此时数组已经有序,无需再进行后续趟数的比较了。此时可以中止比较。

  • 二分法查找

    import java.util.Arrays;
    
    public class TestBinarySearch {
        public static void main(String[] args) {
            int[] arr = {30, 20, 50 ,10, 80, 9, 7, 12, 100, 40, 8};
            Arrays.sort(arr);
            System.out.println(Arrays.toString(arr));
            System.out.println(myBinarySearch(arr, 400));
        }
    
        public static int myBinarySearch(int[] arr, int value){
            /* 二分法查找 */
            int low = 0;
            int high = arr.length - 1;
    
            while (low <= high){
                int mid = (low + high)/2;
    
                if (value == arr[mid]){
                    return mid;
                }
    
                if (value > arr[mid]){
                    low = mid + 1;
                }
    
                if (value < arr[mid]){
                    high = mid - 1;
                }
                System.out.println(low + "+" + high);
    
            }
            return -1;
        }
    }
    
    • 分法检索(binary search)又称折半检索,二分法检索的基本思想是设数组中的元素从小到大有序地存放在数组(array)中,首先将给定值key与数组中间位置上元素的关键码(key)比较,如果相等,则检索成功;
    • 否则,若key小,则在数组前半部分中继续进行二分法检索;
    • 若key大,则在数组后半部分中继续进行二分法检索。
    • 这样,经过一次比较就缩小一半的检索区间,如此进行下去,直到检索成功或检索失败。
    • 二分法检索是一种效率较高的检索方法。

第8章 常用类

1. 包装类

  • Java是面向对象的语言,但并不是“纯面向对象”的,因为我们经常用到的基本数据类型就不是对象。但是我们在实际应用中经常需要将基本数据转化成对象,以便于操作。比如:将基本数据类型存储到Object[]数组或集合中的操作等等。

  • 为了解决这个不足,Java在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。

  • 在这八个类名中,除了Integer和Character类以外,其它六个类的类名和基本数据类型一致,只是类名的第一个字母大写而已。

  • 包装类的用途

    • 对于包装类来说,这些类的用途主要包含两种:

      1. 作为和基本数据类型对应的类型存在,方便涉及到对象的操作,如Object[]、集合等的操作。

      2. 包含每种基本数据类型的相关属性如最大值、最小值等,以及相关的操作方法(这些操作方法的作用是在基本数据类型、包装类对象、字符串之间提供相互之间的转化!)。

  • 自动装箱和拆箱

    • 自动装箱和拆箱就是将基本数据类型和包装类之间进行自动的互相转换。JDK1.5后,Java引入了自动装箱(autoboxing)/拆箱(unboxing)。

    • 自动装箱:基本类型的数据处于需要对象的环境中时,会自动转为“对象”。

      Integer i = 100;//自动装箱
      //相当于编译器自动为您作以下的语法编译:
      Integer i = Integer.valueOf(100);//调用的是valueOf(100),而不是new Integer(100)
      
    • 自动拆箱:每当需要一个值时,对象会自动转成基本数据类型,没必要再去显式调用intValue()、doubleValue()等转型方法。

      Integer i = 100;
      int j = i;//自动拆箱
      //相当于编译器自动为您作以下的语法编译:
      int j = i.intValue();
      
    • 我们可以用一句话总结自动装箱/拆箱:自动装箱过程是通过调用包装类的valueOf()方法实现的,而自动拆箱过程是通过调用包装类的xxxValue()方法实现的(xxx代表对应的基本数据类型,如intValue()、doubleValue()等)。自动装箱与拆箱的功能事实上是编译器来帮的忙,编译器在编译时依据您所编写的语法,决定是否进行装箱或拆箱动作。

  • 包装类空指针异常问题

    public class Test1 {
        public static void main(String[] args) {
            Integer i = null;
            int j = i;
        }
    }
    
    • null表示i没有指向任何对象的实体,但作为对象名称是合法的(不管这个对象名称存是否指向了某个对象的实体)。由于实际上i并没有指向任何对象的实体,所以也就不可能操作intValue()方法,这样上面的写法在运行时就会出现NullPointerException错误。
  • 包装类的缓存问题

    • 整型、char类型所对应的包装类,在自动装箱时,对于-128~127之间的值会进行缓存处理,其目的是提高效率。

    • 缓存处理的原理为:如果数据在-128~127这个区间,那么在类加载时就已经为该区间的每个数值创建了对象,并将这256个对象存放到一个名为cache的数组中。每当自动装箱过程发生时(或者手动调用valueOf()时),就会先判断数据是否在该区间,如果在则直接获取数组中对应的包装类对象的引用,如果不在该区间,则会通过new调用包装类的构造方法来创建对象。

      public class Test3 {
          public static void main(String[] args) {
              Integer in1 = -128;
              Integer in2 = -128;
              System.out.println(in1 == in2);//true 因为123在缓存范围内
              System.out.println(in1.equals(in2));//true
              Integer in3 = 1234;
              Integer in4 = 1234;
              System.out.println(in3 == in4);//false 因为1234不在缓存范围内
              System.out.println(in3.equals(in4));//true
          }
      }
      
    • 内存分析

      图8-7 示例8-9的内存分析图.png

  • 注意

    1. JDK1.5以后,增加了自动装箱与拆箱功能,如:

      Integer i = 100;  int j = new Integer(100);
      
    2. 自动装箱调用的是valueOf()方法,而不是new Integer()方法。

    3. 自动拆箱调用的xxxValue()方法。

    4. 包装类在自动装箱时为了提高效率,对于-128~127之间的值会进行缓存处理。超过范围后,对象之间不能再使用==进行数值的比较,而是使用equals方法。

2. String类

  • String 类对象代表不可变的Unicode字符序列,因此我们可以将String对象称为“不可变对象”。 那什么叫做“不可变对象”呢?指的是对象内部的成员变量的值无法再改变。

  • 我们发现在前面学习String的某些方法,比如:substring()是对字符串的截取操作,但本质是读取原字符串内容生成了新的字符串

  • 在遇到字符串常量之间的拼接时,编译器会做出优化,即在编译期间就会完成字符串的拼接。因此,在使用==进行String对象之间的比较时,我们需要特别注意。

    public class TestString2 {
        public static void main(String[] args) {
            //编译器做了优化,直接在编译的时候将字符串进行拼接
            String str1 = "hello" + " java";//相当于str1 = "hello java";
            String str2 = "hello java";
            System.out.println(str1 == str2);//true
            String str3 = "hello";
            String str4 = " java";
            //编译的时候不知道变量中存储的是什么,所以没办法在编译的时候优化
            String str5 = str3 + str4;
            System.out.println(str2 == str5);//false
        }
    }
    
  • String类常用的方法有:

    1. String类的下述方法能创建并返回一个新的String对象: concat()、 replace()、substring()、 toLowerCase()、 toUpperCase()、trim()。

    2. 提供查找功能的有关方法: endsWith()、 startsWith()、 indexOf()、lastIndexOf()。

    3. 提供比较功能的方法: equals()、equalsIgnoreCase()、compareTo()。

    4. 其它方法: charAt() 、length()。

  • StringBuffer和StringBuilder

    • StringBuffer和StringBuilder非常类似,均代表可变的字符序列。 这两个类都是抽象类AbstractStringBuilder的子类,方法几乎一模一样。

      1. StringBuffer JDK1.0版本提供的类,线程安全,做线程同步检查, 效率较低。

      2. StringBuilder JDK1.5版本提供的类,线程不安全,不做线程同步检查,因此效率较高。 建议采用该类。

    • 常用方法列表:

      1. 重载的public StringBuilder append(…)方法:可以为该StringBuilder 对象添加字符序列,仍然返回自身对象。

      2. 方法 public StringBuilder delete(int start,int end):可以删除从start开始到end-1为止的一段字符序列,仍然返回自身对象。

      3. 方法 public StringBuilder deleteCharAt(int index):移除此序列指定位置上的 char,仍然返回自身对象。

      4. 重载的public StringBuilder insert(…)方法:可以为该StringBuilder 对象在指定位置插入字符序列,仍然返回自身对象。

      5. 方法 public StringBuilder reverse():用于将字符序列逆序,仍然返回自身对象。

      6. 方法 public String toString() 返回此序列中数据的字符串表示形式。

      7. 和 String 类含义类似的方法:

        public int indexOf(String str)
        public int indexOf(String str,int fromIndex)
        public String substring(int start)
        public String substring(int start,int end)
        public int length() 
        char charAt(int index)
        
    • StringBuffer/StringBuilder基本用法

      public class TestStringBufferAndBuilder 1{
          public static void main(String[] args) {
              /**StringBuilder*/
              StringBuilder sb = new StringBuilder();
              for (int i = 0; i < 7; i++) {
                  sb.append((char) ('a' + i));//追加单个字符
              }
              System.out.println(sb.toString());//转换成String输出
              sb.append(", I can sing my abc!");//追加字符串
              System.out.println(sb.toString());
              /**StringBuffer*/
              StringBuffer sb2 = new StringBuffer("中华人民共和国");
              sb2.insert(0, "爱").insert(0, "我");//插入字符串
              System.out.println(sb2);
              sb2.delete(0, 2);//删除子字符串
              System.out.println(sb2);
              sb2.deleteCharAt(0).deleteCharAt(0);//删除某个字符
              System.out.println(sb2.charAt(0));//获取某个字符
              System.out.println(sb2.reverse());//字符串逆序
          }
      }
      
  • 不可变和可变字符序列使用陷阱

    • String使用的陷阱:String一经初始化后,就不会再改变其内容了。对String字符串的操作实际上是对其副本(原始拷贝)的操作,原来的字符串一点都没有改变。比如:

      ​ String s ="a"; 创建了一个字符串

      s = s+"b"; 实际上原来的"a"字符串对象已经丢弃了,现在又产生了另一个字符串s+"b"(也就是"ab")。 如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的时间和空间性能,甚至会造成服务器的崩溃。

    • 相反,StringBuilder和StringBuffer类是对原字符串本身操作的,可以对字符串进行修改而不产生副本拷贝或者产生少量的副本。因此可以在循环中使用。

3. 时间处理相关类

图8-14 日期时间相关类.png

  • Date时间类(java.util.Date)

    • 在标准Java类库中包含一个Date类。它的对象表示一个特定的瞬间,精确到毫秒。

      1. Date() 分配一个Date对象,并初始化此对象为系统当前的日期和时间,可以精确到毫秒)。

      2. Date(long date) 分配 Date 对象并初始化此对象,以表示自从标准基准时间(称为“历元(epoch)”,即 1970 年 1 月 1 日 00:00:00 GMT)以来的指定毫秒数。

      3. boolean after(Date when) 测试此日期是否在指定日期之后。

      4. booleanbefore(Date when) 测试此日期是否在指定日期之前。

      5. boolean equals(Object obj) 比较两个日期的相等性。

      6. long getTime() 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。

      7. String toString() 把此 Date 对象转换为以下形式的 String:

      ​ dow mon dd hh:mm:ss zzz yyyy 其中: dow 是一周中的某一天 (Sun、 Mon、Tue、Wed、 Thu、 Fri、 Sat)。

  • DateFormat类的作用

    • 把时间对象转化成指定格式的字符串。反之,把指定格式的字符串转化成时间对象。

    • DateFormat是一个抽象类,一般使用它的的子类SimpleDateFormat类来实现。

      import java.text.ParseException;
      import java.text.SimpleDateFormat;
      import java.util.Date;
      public class TestDateFormat {
          public static void main(String[] args) throws ParseException {
              // new出SimpleDateFormat对象
              SimpleDateFormat s1 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
              SimpleDateFormat s2 = new SimpleDateFormat("yyyy-MM-dd");
              // 将时间对象转换成字符串
              String daytime = s1.format(new Date());
              System.out.println(daytime);
              System.out.println(s2.format(new Date()));
              System.out.println(new SimpleDateFormat("hh:mm:ss").format(new Date()));
              // 将符合指定格式的字符串转成成时间对象.字符串格式需要和指定格式一致。
              String time = "2007-10-7";
              Date date = s2.parse(time);
              System.out.println("date1: " + date);
              time = "2007-10-7 20:15:30";
              date = s1.parse(time);
              System.out.println("date2: " + date);
          }
      }
      
    • 代码中的格式化字符的具体含义:

      表8-2 æ ¼å¼åŒ–å­—ç¬¦çš„å«ä¹‰.png

    • 时间格式字符也可以为我们提供其他的便利。比如:获得当前时间是今年的第几天。

      import java.text.SimpleDateFormat;
      import java.util.Date;
      public class TestDateFormat2 {
          public static void main(String[] args) {
              SimpleDateFormat s1 = new SimpleDateFormat("D");
              String daytime = s1.format(new Date());
              System.out.println(daytime);
          }
      }
      
  • Calendar日历类

    • Calendar 类是一个抽象类,为我们提供了关于日期计算的相关功能,比如:年、月、日、时、分、秒的展示和计算。

    • GregorianCalendar 是 Calendar 的一个具体子类,提供了世界上大多数国家/地区使用的标准日历系统。

    • 注意月份的表示,一月是0,二月是1,以此类推,12月是11。

    • GregorianCalendar类和Calendar类的使用

      import java.util.*;
      public class TestCalendar {
          public static void main(String[] args) {
              // 得到相关日期元素
              GregorianCalendar calendar = new GregorianCalendar(2999, 10, 9, 22, 10, 50);
              int year = calendar.get(Calendar.YEAR); // 打印:1999
              int month = calendar.get(Calendar.MONTH); // 打印:10
              int day = calendar.get(Calendar.DAY_OF_MONTH); // 打印:9
              int day2 = calendar.get(Calendar.DATE); // 打印:9
              // 日:Calendar.DATE和Calendar.DAY_OF_MONTH同义
              int date = calendar.get(Calendar.DAY_OF_WEEK); // 打印:3
              // 星期几 这里是:1-7.周日是1,周一是2,。。。周六是7
              System.out.println(year);
              System.out.println(month);
              System.out.println(day);
              System.out.println(day2);
              System.out.println(date);
              // 设置日期
              GregorianCalendar calendar2 = new GregorianCalendar();
              calendar2.set(Calendar.YEAR, 2999);
              calendar2.set(Calendar.MONTH, Calendar.FEBRUARY); // 月份数:0-11
              calendar2.set(Calendar.DATE, 3);
              calendar2.set(Calendar.HOUR_OF_DAY, 10);
              calendar2.set(Calendar.MINUTE, 20);
              calendar2.set(Calendar.SECOND, 23);
              printCalendar(calendar2);
              // 日期计算
              GregorianCalendar calendar3 = new GregorianCalendar(2999, 10, 9, 22, 10, 50);
              calendar3.add(Calendar.MONTH, -7); // 月份减7
              calendar3.add(Calendar.DATE, 7); // 增加7天
              printCalendar(calendar3);
              // 日历对象和时间对象转化
              Date d = calendar3.getTime();
              GregorianCalendar calendar4 = new GregorianCalendar();
              calendar4.setTime(new Date());
              long g = System.currentTimeMillis();
          }
          static void printCalendar(Calendar calendar) {
              int year = calendar.get(Calendar.YEAR);
              int month = calendar.get(Calendar.MONTH) + 1;
              int day = calendar.get(Calendar.DAY_OF_MONTH);
              int date = calendar.get(Calendar.DAY_OF_WEEK) - 1; // 星期几
              String week = "" + ((date == 0) ? "日" : date);
              int hour = calendar.get(Calendar.HOUR);
              int minute = calendar.get(Calendar.MINUTE);
              int second = calendar.get(Calendar.SECOND);
              System.out.printf("%d年%d月%d日,星期%s %d:%d:%d\n", year, month, day,  
                              week, hour, minute, second);
          }
      }
      
    • 可视化日历的编写

      import java.text.ParseException;
      import java.util.Calendar;
      import java.util.GregorianCalendar;
      import java.util.Scanner;
      public class TestCalendar2 {
          public static void main(String[] args) throws ParseException {
              System.out.println("请输入日期(格式为:2010-3-3):");
              Scanner scanner = new Scanner(System.in);
              String dateString = scanner.nextLine(); // 2010-3-1
              // 将输入的字符串转化成日期类
              System.out.println("您刚刚输入的日期是:" + dateString);
              String[] str = dateString.split("-");
              int year = Integer.parseInt(str[0]);
              int month = new Integer(str[1]);
              int day = new Integer(str[2]);
              Calendar c = new GregorianCalendar(year, month - 1, day); // Month:0-11
              // 大家自己补充另一种方式:将字符串通过SImpleDateFormat转化成Date对象,
              //再将Date对象转化成日期类
              // SimpleDateFormat sdfDateFormat = new SimpleDateFormat("yyyy-MM-dd");
              // Date date = sdfDateFormat.parse(dateString);
              // Calendar c = new GregorianCalendar();
              // c.setTime(date);
              // int day = c.get(Calendar.DATE);
              c.set(Calendar.DATE, 1);
              int dow = c.get(Calendar.DAY_OF_WEEK); // week:1-7 日一二三四五六
              System.out.println("日\t一\t二\t三\t四\t五\t六");
              for (int i = 0; i < dow - 1; i++) {
                  System.out.print("\t");
              }
              int maxDate = c.getActualMaximum(Calendar.DATE);
              // System.out.println("maxDate:"+maxDate);
              for (int i = 1; i <= maxDate; i++) {
                  StringBuilder sBuilder = new StringBuilder();
                  if (c.get(Calendar.DATE) == day) {
                      sBuilder.append(c.get(Calendar.DATE) + "*\t");
                  } else {
                      sBuilder.append(c.get(Calendar.DATE) + "\t");
                  }
                  System.out.print(sBuilder);
                  // System.out.print(c.get(Calendar.DATE)+
                  //                ((c.get(Calendar.DATE)==day)?"*":"")+"\t");
       
                  if (c.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {
                      System.out.print("\n");
                  }
                  c.add(Calendar.DATE, 1);
              }
          }
      }
      

4. Math类

  • java.lang.Math提供了一系列静态方法用于科学计算;其方法的参数和返回值类型一般为double型。如果需要更加强大的数学运算能力,计算高等数学中的相关内容,可以使用apache commons下面的Math类库。

  • Math类的常用方法:

    1. abs 绝对值

    2. acos,asin,atan,cos,sin,tan 三角函数

    3. sqrt 平方根

    4. pow(double a, double b) a的b次幂

    5. max(double a, double b) 取大值

    6. min(double a, double b) 取小值

    7. ceil(double a) 大于a的最小整数

    8. floor(double a) 小于a的最大整数

    9. random() 返回 0.0 到 1.0 的随机数

    10. long round(double a) double型的数据a转换为long型(四舍五入)

    11. toDegrees(double angrad) 弧度->角度

    12. toRadians(double angdeg) 角度->弧度

  • Random类的常用方法

    import java.util.Random;
    public class TestRandom {
        public static void main(String[] args) {
            Random rand = new Random();
            //随机生成[0,1)之间的double类型的数据
            System.out.println(rand.nextDouble());
            //随机生成int类型允许范围之内的整型数据
            System.out.println(rand.nextInt());
            //随机生成[0,1)之间的float类型的数据
            System.out.println(rand.nextFloat());
            //随机生成false或者true
            System.out.println(rand.nextBoolean());
            //随机生成[0,10)之间的int类型的数据
            System.out.print(rand.nextInt(10));
            //随机生成[20,30)之间的int类型的数据
            System.out.print(20 + rand.nextInt(10));
            //随机生成[20,30)之间的int类型的数据(此种方法计算较为复杂)
            System.out.print(20 + (int) (rand.nextDouble() * 10));
        }
    }
    
    • Math类中虽然为我们提供了产生随机数的方法Math.random(),但是通常我们需要的随机数范围并不是[0, 1)之间的double类型的数据,这就需要对其进行一些复杂的运算。
    • 如果使用Math.random()计算过于复杂的话,我们可以使用例外一种方式得到随机数,即Random类,这个类是专门用来生成随机数的,并且Math.random()底层调用的就是Random的nextDouble()方法。
    • Random类位于java.util包下。

5. File类

  • java.io.File类:代表文件和目录。 在开发中,读取文件、生成文件、删除文件、修改文件的属性时经常会用到本类。

  • File类的常见构造方法:public File(String pathname)

    • 以pathname为路径创建File对象,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储

    • 文件的创建

      import java.io.File;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: pycharm
       * @file: FileTest1.java
       * @time: 2019/9/22 15:09
       * @desc: File类的常见构造方法
       */
      
      public class FileTest1{
          public static void main(String[] args) throws Exception{
              test1();
          }
      
          public static void test1() throws Exception{
              System.out.println(System.getProperty("user.dir"));
      
              // 相对路径,默认放到user.dir目录下
              File f = new File("a.txt");
              // 创建文件
              f.createNewFile();
              // 绝对路径
              File f2 = new File("D:\\李添的数据哦!!!\\BookStudy\\else\\JavaWorkSpace\\b.txt");
              f2.createNewFile();
          }
      
      }
      
  • 通过File对象可以访问文件的属性:

    表8-3 File类访问属性的方法列表.png

  • 测试File类访问属性的基本用法

    import java.io.File;
    import java.util.Date;
    public class TestFile2 {
        public static void main(String[] args) throws Exception {
            File f = new File("d:/b.txt");
            System.out.println("File是否存在:"+f.exists());
            System.out.println("File是否是目录:"+f.isDirectory());
            System.out.println("File是否是文件:"+f.isFile());
            System.out.println("File最后修改时间:"+new Date(f.lastModified()));
            System.out.println("File的大小:"+f.length());
            System.out.println("File的文件名:"+f.getName());
            System.out.println("File的目录路径:"+f.getPath());
        }
    }
    
  • 通过File对象创建空文件或目录(在该对象所指的文件或目录不存在的情况下)

    表8-4 File类创建文件或目录的方法列表.png

  • 使用mkdir创建目录

    import java.io.File;
    public class TestFile3 {
        public static void main(String[] args) throws Exception {
            File f = new File("d:/c.txt");
            f.createNewFile(); // 会在d盘下面生成c.txt文件
            f.delete(); // 将该文件或目录从硬盘上删除
            File f2 = new File("d:/电影/华语/大陆");
            boolean flag = f2.mkdir(); //目录结构中有一个不存在,则不会创建整个目录树
            System.out.println(flag);//创建失败
        }
    }
    
  • 使用mkdirs创建目录

    import java.io.File;
    public class TestFile4 {
        public static void main(String[] args) throws Exception {
            File f = new File("d:/c.txt");
            f.createNewFile(); // 会在d盘下面生成c.txt文件
            f.delete(); // 将该文件或目录从硬盘上删除
            File f2 = new File("d:/电影/华语/大陆");
            boolean flag = f2.mkdirs();//目录结构中有一个不存在也没关系;创建整个目录树
            System.out.println(flag);//创建成功
        }
    }
    
  • File类的综合应用

    import java.io.File;
    import java.io.IOException;
    public class TestFile5 {
        public static void main(String[] args) {
            //指定一个文件
            File file = new File("d:/sxt/b.txt");
            //判断该文件是否存在
            boolean flag= file.exists();
            //如果存在就删除,如果不存在就创建
            if(flag){
                //删除
                boolean flagd = file.delete();
                if(flagd){
                    System.out.println("删除成功");
                }else{
                    System.out.println("删除失败");
                }
            }else{
                //创建
                boolean flagn = true;
                try {
                    //如果目录不存在,先创建目录
                    File dir = file.getParentFile();
                    dir.mkdirs();
                    //创建文件
                    flagn = file.createNewFile();
                    System.out.println("创建成功");
                } catch (IOException e) {
                    System.out.println("创建失败");
                    e.printStackTrace();
                }          
            }
            //文件重命名(同学可以自己测试一下)
            //file.renameTo(new File("d:/readme.txt"));
        }
    }
    
  • 递归遍历目录结构和树状展现

    import java.io.File;
    public class TestFile6 {
        public static void main(String[] args) {
            File f = new File("d:/电影");
            printFile(f, 0);
        }
        /**
         * 打印文件信息
         * @param file 文件名称
         * @param level 层次数(实际就是:第几次递归调用)
         */
        static void printFile(File file, int level) {
            //输出层次数
            for (int i = 0; i < level; i++) {
                System.out.print("-");
            }
            //输出文件名
            System.out.println(file.getName());
            //如果file是目录,则获取子文件列表,并对每个子文件进行相同的操作
            if (file.isDirectory()) {
                File[] files = file.listFiles();
                for (File temp : files) {
                    //递归调用该方法:注意等+1
                    printFile(temp, level + 1);
                }
            }
        }
    }
    

6. 枚举

  • JDK1.5引入了枚举类型。枚举类型的定义包括枚举声明和枚举体。格式如下:

    enum  枚举名 {
          枚举体(常量列表)
    }
    
  • 枚举体就是放置一些常量。我们可以写出我们的第一个枚举类型。

    enum Season {
        SPRING, SUMMER, AUTUMN, WINDER 
    }
    
  • 所有的枚举类型隐性地继承自 java.lang.Enum。枚举实质上还是类!而每个被枚举的成员实质就是一个枚举类型的实例,他们默认都是public static final修饰的。可以直接通过枚举类型名使用它们。

  • 注意:

    1. 当你需要定义一组常量时,可以使用枚举类型。

    2. 尽量不要使用枚举的高级特性,事实上高级特性都可以使用普通类来实现,没有必要引入枚举,增加程序的复杂性!

  • 枚举的使用

    import java.util.Random;
    public class TestEnum {
        public static void main(String[] args) {
            // 枚举遍历
            for (Week k : Week.values()) {
                System.out.println(k);
            }
            // switch语句中使用枚举
            int a = new Random().nextInt(4); // 生成0,1,2,3的随机数
            switch (Season.values()[a]) {
            case SPRING:
                System.out.println("春天");
                break;
            case SUMMER:
                System.out.println("夏天");
                break;
            case AUTUMN:
                System.out.println("秋天");
                break;
            case WINDTER:
                System.out.println("冬天");
                break;
            }
        }
    }
    /**季节*/
    enum Season {
        SPRING, SUMMER, AUTUMN, WINDTER
    }
    /**星期*/
    enum Week {
        星期一, 星期二, 星期三, 星期四, 星期五, 星期六, 星期日
    }
    

第9章 容器

  • 什么是“容器”呢?生活中的容器不难理解,是用来容纳物体的,如锅碗瓢盆、箱子和包等。程序中的“容器”也有类似的功能,就是用来容纳和管理数据。

  • 数组就是一种容器,可以在其中放置对象或基本类型数据。

  • 数组的优势:是一种简单的线性序列,可以快速地访问数组元素,效率高。如果从效率和类型检查的角度讲,数组是最好的。

  • 数组的劣势:不灵活。容量需要事先定义好,不能随着需求的变化而扩容。

  • 容器,也叫集合(Collection)。

    图9-1容器的接口层次结构图.png

1. 泛型

  • 泛型是JDK1.5以后增加的,它可以帮助我们建立类型安全的集合。在使用了泛型的集合中,遍历时不必进行强制类型转换。JDK提供了支持泛型的编译器,将运行时的类型检查提前到了编译时执行,提高了代码可读性和安全性。

  • 泛型的本质就是“数据类型的参数化”。 我们可以把“泛型”理解为数据类型的一个占位符(形式参数),即告诉编译器,在调用泛型时必须传入实际类型。

  • 我们可以在类的声明处增加泛型列表,如:<T,E,V>。

  • 此处,字符可以是任何标识符,一般采用这3个字母。

  • 自定义泛型

    • 泛型类的声明

      class MyCollection<E> {// E:表示泛型;
          Object[] objs = new Object[5];
       
          public E get(int index) {// E:表示泛型;
              return (E) objs[index];
          }
          public void set(E e, int index) {// E:表示泛型;
              objs[index] = e;
          }
      }
      
    • 泛型E像一个占位符一样表示“未知的某个数据类型”,我们在真正调用的时候传入这个“数据类型”。

    • 泛型类的应用

      public class TestGenerics {
          public static void main(String[] args) {
              // 这里的”String”就是实际传入的数据类型;
              MyCollection<String> mc = new MyCollection<String>();
              mc.set("aaa", 0);
              mc.set("bbb", 1);
              String str = mc.get(1); //加了泛型,直接返回String类型,不用强制转换;
              System.out.println(str);
          }
      }
      
  • 容器中使用泛型

    • 通过阅读源码,我们发现Collection、List、Set、Map、Iterator接口都定义了泛型。因此,我们在使用这些接口及其实现类时,都强烈建议使用泛型。(事实上,不使用编译器也不会报错!)

2. Collection接口

  • Collection 表示一组对象,它是集中、收集的意思。Collection接口的两个子接口是List、Set接口。

  • Collection接口中定义的方法

    表9-1 Collection接口中定义的方法.png

  • 由于List、Set是Collection的子接口,意味着所有List、Set的实现类都有上面的方法。

3. List

  • List是有序、可重复的容器。

  • 有序:List中每个元素都有索引标记。可以根据元素的索引标记(在List中的位置)访问元素,从而精确控制这些元素。

  • 可重复:List允许加入重复的元素。更确切地讲,List通常允许满足 e1.equals(e2) 的元素重复加入容器。

  • 除了Collection接口中的方法,List多了一些跟顺序(索引)有关的方法

    表9-2 List接口中定义的方法.png

  • List接口常用的实现类有3个:ArrayList、LinkedList和Vector。

  • List的常用方法

    public class TestList {
        /**
         * 测试add/remove/size/isEmpty/contains/clear/toArrays等方法
         */
        public static void test01() {
            List<String> list = new ArrayList<String>();
            System.out.println(list.isEmpty()); // true,容器里面没有元素
            list.add("高淇");
            System.out.println(list.isEmpty()); // false,容器里面有元素
            list.add("高小七");
            list.add("高小八");
            System.out.println(list);
            System.out.println("list的大小:" + list.size());
            System.out.println("是否包含指定元素:" + list.contains("高小七"));
            list.remove("高淇");
            System.out.println(list);
            Object[] objs = list.toArray();
            System.out.println("转化成Object数组:" + Arrays.toString(objs));
            list.clear();
            System.out.println("清空所有元素:" + list);
        }
        public static void main(String[] args) {
            test01();
        }
    }
    
  • 两个List之间的元素处理

    public class TestList {
        public static void main(String[] args) {
            test02();
        }
        /**
         * 测试两个容器之间元素处理
         */
        public static void test02() {
            List<String> list = new ArrayList<String>();
            list.add("高淇");
            list.add("高小七");
            list.add("高小八");
     
            List<String> list2 = new ArrayList<String>();
            list2.add("高淇");
            list2.add("张三");
            list2.add("李四");
            System.out.println(list.containsAll(list2)); //false list是否包含list2中所有元素
            System.out.println(list);
            list.addAll(list2); //将list2中所有元素都添加到list中
            System.out.println(list);
            list.removeAll(list2); //从list中删除同时在list和list2中存在的元素
            System.out.println(list);
            list.retainAll(list2); //取list和list2的交集
            System.out.println(list);
        }
    }
    
  • List中操作索引的常用方法

    public class TestList {
        public static void main(String[] args) {
            test03();
        }
        /**
         * 测试List中关于索引操作的方法
         */
        public static void test03() {
            List<String> list = new ArrayList<String>();
            list.add("A");
            list.add("B");
            list.add("C");
            list.add("D");
            System.out.println(list); // [A, B, C, D]
            list.add(2, "高");
            System.out.println(list); // [A, B, 高, C, D]
            list.remove(2);
            System.out.println(list); // [A, B, C, D]
            list.set(2, "c");
            System.out.println(list); // [A, B, c, D]
            System.out.println(list.get(1)); // 返回:B
            list.add("B");
            System.out.println(list); // [A, B, c, D, B]
            System.out.println(list.indexOf("B")); // 1 从头到尾找到第一个"B"
            System.out.println(list.lastIndexOf("B")); // 4 从尾到头找到第一个"B"
        }
    }
    
  • ArrayList特点和底层实现

    • ArrayList底层是用数组实现的存储。 特点:查询效率高,增删效率低(LinkedList高),线程不安全(Vector安全)。我们一般使用它。
    • 我们知道,数组长度是有限的,而ArrayList是可以存放任意数量的对象,长度不受限制,那么它是怎么实现的呢?本质上就是通过定义新的更大的数组,将旧数组中的内容拷贝到新数组,来实现扩容。 ArrayList的Object数组初始化长度为10,如果我们存储满了这个数组,需要存储第11个对象,就会定义新的长度更大的数组,并将原数组内容和新的元素一起加入到新数组中。
  • LinkedList特点和底层实现

    • LinkedList底层用双向链表实现的存储。特点:查询效率低,增删效率高,线程不安全。

    • 双向链表也叫双链表,是链表的一种,它的每个数据节点中都有两个指针,分别指向前一个节点和后一个节点。 所以,从双向链表中的任意一个节点开始,都可以很方便地找到所有节点。

      图9-8 LinkedList的存储结构图.png

    • 每个节点都应该有3部分内容:

      class  Node {
      	Node  previous;     //前一个节点
      	Object  element;    //本节点保存的数据
      	Node  next;         //后一个节点
      }
      
  • Vector向量

    • Vector底层是用数组实现的List,相关的方法都加了同步检查,因此“线程安全,效率低”。 比如,indexOf方法就增加了synchronized同步标记。
  • 如何选用ArrayList、LinkedList、Vector?

    1. 需要线程安全时,用Vector。

    2. 不存在线程安全问题时,并且查找较多用ArrayList(一般使用它)。

    3. 不存在线程安全问题时,增加或删除元素较多用LinkedList。

4. Map

  • Map接口

    • Map就是用来存储“键(key)-值(value) 对”的。 Map类中存储的“键值对”通过键来标识,所以“键对象”不能重复。

    • Map 接口的实现类有HashMap、TreeMap、HashTable、Properties等。

    • Map接口中常用的方法

      表9-3 Map接口中常用的方法.png

  • HashMap和HashTable

    • HashMap采用哈希算法实现,是Map接口最常用的实现类。 由于底层采用了哈希表存储数据,我们要求键不能重复,如果发生重复,新的键值对会替换旧的键值对。 HashMap在查找、删除、修改方面都有非常高的效率。

      import java.util.HashMap;
      import java.util.Map;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: pycharm
       * @file: TestMap.java
       * @time: 2019/10/5 14:05
       * @desc:
       */
      
      public class TestMap {
          public static void main(String[] args){
              Map<Integer, String> m1 = new HashMap<>();
      
              m1.put(1, "one");
              m1.put(2, "two");
              m1.put(3, "three");
      
              System.out.println(m1.get(1));
              System.out.println(m1.size());
              System.out.println(m1.isEmpty());
              System.out.println(m1.containsKey(2));
              System.out.println(m1.containsValue("four"));
      
              Map<Integer, String> m2 = new HashMap<>();
              m2.put(4, "四");
              m2.put(5, "五");
              m1.putAll(m2);
      
              System.out.println(m1);
      
              // map中键不能重复!如果重复(是否重复是根据equals方法),则新的覆盖旧的!
              m1.put(3, "三");
              System.out.println(m1);
          }
      }
      
    • HashTable类和HashMap用法几乎一样,底层实现几乎一样,只不过HashTable的方法添加了synchronized关键字确保线程同步检查,效率较低。

    • HashMap与HashTable的区别

      1. HashMap: 线程不安全,效率高。允许key或value为null。

      2. HashTable: 线程安全,效率低。不允许key或value为null。

  • HashMap底层实现详解

    • HashMap底层实现采用了哈希表,这是一种非常重要的数据结构。对于我们以后理解很多技术都非常有帮助(比如:redis数据库的核心技术和HashMap一样)。

    • 数据结构中由数组和链表来实现对数据的存储,他们各有特点。

      ​ (1) 数组:占用空间连续。 寻址容易,查询速度快。但是,增加和删除效率非常低。

      ​ (2) 链表:占用空间不连续。 寻址困难,查询速度慢。但是,增加和删除效率非常高。

    • 那么,我们能不能结合数组和链表的优点(即查询快,增删效率也高)呢? 答案就是“哈希表”。 哈希表的本质就是“数组+链表”。

    • Entry数组存储结构图图9-15 Entry数组存储结构图.png

    • 存储数据过程put(key,value)

      图9-16 HashMap存储数据过程示意图.png

    • 我们的目的是将“key-value两个对象”成对存放到HashMap的Entry[]数组中。参见以下步骤:

      1. 获得key对象的hashcode:首先调用key对象的hashcode()方法,获得hashcode。

      2. 根据hashcode计算出hash值(要求在[0, 数组长度-1]区间):hashcode是一个整数,我们需要将它转化成[0, 数组长度-1]的范围。我们要求转化后的hash值尽量均匀地分布在[0,数组长度-1]这个区间,减少“hash冲突”。

        • 一种极端简单和低下的算法是:hash值 = hashcode/hashcode

          也就是说,hash值总是1。意味着,键值对对象都会存储到数组索引1位置,这样就形成一个非常长的链表。相当于每存储一个对象都会发生“hash冲突”,HashMap也退化成了一个“链表”。

        • 一种简单和常用的算法是(相除取余算法):hash值 = hashcode%数组长度

          这种算法可以让hash值均匀的分布在[0,数组长度-1]的区间。 早期的HashTable就是采用这种算法。但是,这种算法由于使用了“除法”,效率低下。JDK后来改进了算法。首先约定数组长度必须为2的整数幂,这样采用位运算即可实现取余的效果:hash值 = hashcode&(数组长度-1)。

        • 事实上,为了获得更好的散列效果,JDK对hashcode进行了两次散列处理(核心目标就是为了分布更散更均匀)

      3. 生成Entry对象:如上所述,一个Entry对象包含4部分:key对象、value对象、hash值、指向下一个Entry对象的引用。我们现在算出了hash值。下一个Entry对象的引用为null。

      4. 将Entry对象放到table数组中:如果本Entry对象对应的数组索引位置还没有放Entry对象,则直接将Entry对象存储进数组;如果对应索引位置已经有Entry对象,则将已有Entry对象的next指向本Entry对象,形成链表。

    • 总结如上过程:当添加一个元素(key-value)时,首先计算key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,就形成了链表,同一个链表上的Hash值是相同的,所以说数组存放的是链表。 JDK8中,当链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。

    • 取数据过程get(key):我们需要通过key对象获得“键值对”对象,进而返回value对象。明白了存储数据过程,取数据就比较简单了,参见以下步骤:

      1. 获得key的hashcode,通过hash()散列算法得到hash值,进而定位到数组的位置。
      2. 在链表上挨个比较key对象。 调用equals()方法,将key对象和链表上所有节点的key对象进行比较,直到碰到返回true的节点对象为止。
      3. 返回equals()为true的节点对象的value对象。
    • hashcode()和equals方法的关系:Java中规定,两个内容相同(equals()为true)的对象必须具有相等的hashCode。因为如果equals()为true而两个对象的hashcode不同;那在整个存储过程中就发生了悖论。

    • 扩容问题:HashMap的位桶数组,初始大小为16。实际使用时,显然大小是可变的。如果位桶数组中的元素达到(0.75*数组 length), 就重新调整数组大小变为原来2倍大小。扩容很耗时。扩容的本质是定义新的更大的数组,并将旧数组内容挨个拷贝到新数组中。

    • JDK8将链表在大于8情况下变为红黑二叉树:JDK8中,HashMap在存储一个元素时,当对应链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。

  • 二叉树和红黑二叉树

    • 二叉树(BinaryTree)由一个节点及两棵互不相交的、分别称作这个根的左子树和右子树的二叉树组成。

    • 二叉树的左子树和右子树是严格区分并且不能随意颠倒的。

    • 排序二叉树特性如下:

      ​ (1) 左子树上所有节点的值均小于它的根节点的值。

      ​ (2) 右子树上所有节点的值均大于它的根节点的值。

    • 平衡二叉树(AVL)

      • 为了避免出现上述一边倒的存储,科学家提出了“平衡二叉树”。
      • 在平衡二叉树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。 增加和删除节点可能需要通过一次或多次树旋转来重新平衡这个树。
      • 在平衡二叉树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。 增加和删除节点可能需要通过一次或多次树旋转来重新平衡这个树。
      • 平衡二叉树追求绝对平衡,实现起来比较麻烦,每次插入新节点需要做的旋转操作次数不能预知。
    • 红黑二叉树

      • 红黑二叉树(简称:红黑树),它首先是一棵二叉树,同时也是一棵自平衡的排序二叉树。

      • 红黑树在原有的排序二叉树增加了如下几个要求:

        1. 每个节点要么是红色,要么是黑色。

        2. 根节点永远是黑色的。

        3. 所有的叶节点都是空节点(即 null),并且是黑色的。

        4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)

        5. 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。

      • 这些约束强化了红黑树的关键性质:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。这样就让树大致上是平衡的。

      • 红黑树是一个更高效的检索二叉树,JDK 提供的集合类 TreeMap、TreeSet 本身就是一个红黑树的实现。

      • 红黑树的基本操作:插入、删除、左旋、右旋、着色。 每插入或者删除一个节点,可能会导致树不在符合红黑树的特征,需要进行修复,进行 “左旋、右旋、着色”操作,使树继续保持红黑树的特性。

  • TreeMap的使用和底层实现

    • TreeMap是红黑二叉树的典型实现。
    • TreeMap和HashMap实现了同样的接口Map,因此,用法对于调用者来说没有区别。HashMap效率高于TreeMap;在需要排序的Map时才选用TreeMap。

5. Set

  • Set接口

    • Set接口继承自Collection,Set接口中没有新增方法,方法和Collection保持完全一致。我们在前面通过List学习的方法,在Set中仍然适用。
    • Set容器特点:无序、不可重复。无序指Set中的元素没有索引,我们只能遍历查找;不可重复指不允许加入重复的元素。更确切地讲,新元素如果和Set中某个元素通过equals()方法对比为true,则不能加入;甚至,Set中也只能放入一个null元素,不能多个。
    • Set常用的实现类有:HashSet、TreeSet等,我们一般使用HashSet。
  • HashSet基本使用

    • 重点体会“Set是无序、不可重复”的核心要点。

      public class Test {
          public static void main(String[] args) {
              Set<String> s = new HashSet<String>();
              s.add("hello");
              s.add("world");
              System.out.println(s);
              s.add("hello"); //相同的元素不会被加入
              System.out.println(s);
              s.add(null);
              System.out.println(s);
              s.add(null);
              System.out.println(s);
          }
      }
      
  • HashSet底层实现

    • HashSet是采用哈希算法实现,底层实际是用HashMap实现的(HashSet本质就是一个简化版的HashMap),因此,查询效率和增删效率都比较高。
    • 说白了,就是“往set中加入元素,本质就是把这个元素作为key加入到了内部的map中”。
    • 由于map中key都是不可重复的,因此,Set天然具有“不可重复”的特性。
  • TreeSet的使用和底层实现

    • TreeSet底层实际是用TreeMap实现的,内部维持了一个简化版的TreeMap,通过key来存储Set的元素。 TreeSet内部需要对存储的元素进行排序,因此,我们对应的类需要实现Comparable接口。这样,才能根据compareTo()方法比较对象之间的大小,才能进行内部排序。

      public class Test {
          public static void main(String[] args) {
              User u1 = new User(1001, "高淇", 18);
              User u2 = new User(2001, "高希希", 5);
              Set<User> set = new TreeSet<User>();
              set.add(u1);
              set.add(u2);
          }
      }
       
      class User implements Comparable<User> {
          int id;
          String uname;
          int age;
       
          public User(int id, String uname, int age) {
              this.id = id;
              this.uname = uname;
              this.age = age;
          }
          /**
           * 返回0 表示 this == obj 返回正数表示 this > obj 返回负数表示 this < obj
           */
          @Override
          public int compareTo(User o) {
              if (this.id > o.id) {
                  return 1;
              } else if (this.id < o.id) {
                  return -1;
              } else {
                  return 0;
              }
          }
      }
      
    • 使用TreeSet要点:

      ​ (1) 由于是二叉树,需要对元素做内部排序。 如果要放入TreeSet中的类没有实现Comparable接口,则会抛出异常:java.lang.ClassCastException。

      ​ (2) TreeSet中不能放入null元素。

6. 迭代器

  • 迭代器为我们提供了统一的遍历容器的方式。

  • 如果遇到遍历容器时,判断删除元素的情况,使用迭代器遍历!

    public class Test {
        public static void main(String[] args) {
            List<String> aList = new ArrayList<String>();
            for (int i = 0; i < 5; i++) {
                aList.add("a" + i);
            }
            System.out.println(aList);
            for (Iterator<String> iter = aList.iterator(); iter.hasNext();) {
                String temp = iter.next();
                System.out.print(temp + "\t");
                if (temp.endsWith("3")) {// 删除3结尾的字符串
                    iter.remove();
                }
            }
            System.out.println();
            System.out.println(aList);
        }
    }
    
  • 遍历集合的方法总结

    import java.util.*;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: TestIterator.java
     * @time: 2019/10/8 14:57
     * @desc: 迭代器学习
     */
    
    public class TestIterator {
        public static void main(String[] args){
            testIteratorList();
            testIteratorSet();
            testIteratorMap1();
            testIteratorMap2();
        }
    
        // 遍历List
        public static void testIteratorList(){
            List<String> list = new ArrayList<>();
            list.add("aa");
            list.add("bb");
            list.add("cc");
    
            for(Iterator<String> iter=list.iterator(); iter.hasNext();){
                String temp = iter.next();
                System.out.println(temp);
            }
        }
    
        // 遍历Set
        public static void testIteratorSet(){
            Set<String> set = new HashSet<>();
            set.add("aa");
            set.add("bb");
            set.add("cc");
    
            for(Iterator<String> iter=set.iterator(); iter.hasNext();){
                String temp = iter.next();
                System.out.println(temp);
            }
        }
    
        // 遍历Map:方法1
        public static void testIteratorMap1(){
            Map<Integer, String> map = new HashMap<>();
            map.put(100, "aa");
            map.put(200, "bb");
            map.put(300, "cc");
    
            Set<Map.Entry<Integer, String>> ss = map.entrySet();
            for(Iterator<Map.Entry<Integer, String>> iter = ss.iterator(); iter.hasNext();){
                Map.Entry<Integer, String> temp = iter.next();
                System.out.println(temp.getKey() + "--" + temp.getValue());
            }
        }
    
        // 遍历Map:方法2
        public static void testIteratorMap2(){
            Map<Integer, String> map = new HashMap<>();
            map.put(100, "aa");
            map.put(200, "bb");
            map.put(300, "cc");
    
            Set<Integer> keySet = map.keySet();
    
            for(Iterator<Integer> iter = keySet.iterator(); iter.hasNext();){
                Integer key = iter.next();
                System.out.println(key + "--" + map.get(key));
            }
        }
    }
    

7. Collections辅助类

  • 类 java.util.Collections 提供了对Set、List、Map进行排序、填充、查找元素的辅助方法。

    1. void sort(List) //对List容器内的元素排序,排序的规则是按照升序进行排序。

    2. void shuffle(List) //对List容器内的元素进行随机排列。

    3. void reverse(List) //对List容器内的元素进行逆续排列 。

    4. void fill(List, Object) //用一个特定的对象重写整个List容器。

    5. int binarySearch(List, Object)//对于顺序的List容器,采用折半查找的方法查找特定对象。

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: TestCollections.java
     * @time: 2019/10/8 15:24
     * @desc: 学习Collections辅助类
     */
    
    public class TestCollections {
        public static void main(String[] args){
            List<String> list = new ArrayList<>();
            for(int i =0; i<10; i++){
                list.add("li" + i);
            }
            System.out.println(list);
    
            // 随机排列list中的元素
            Collections.shuffle(list);
            System.out.println(list);
            // 逆序排列
            Collections.reverse(list);
            System.out.println(list);
            // 递增排序
            Collections.sort(list);
            System.out.println(list);
            // 二分查找
            System.out.println(Collections.binarySearch(list, "li"));
            System.out.println(Collections.binarySearch(list, "li2"));
    
        }
    }
    

第10章 IO技术

1. IO入门

  • 对于任何程序设计语言而言,输入输出(Input/Output)系统都是非常核心的功能。程序运行需要数据,数据的获取往往需要跟外部系统进行通信,外部系统可能是文件、数据库、其他程序、网络、IO设备等等。外部系统比较复杂多变,那么我们有必要通过某种手段进行抽象、屏蔽外部的差异,从而实现更加便捷的编程。

  • 数据源:数据源data source,提供数据的原始媒介。常见的数据源有:数据库、文件、其他程序、内存、网络连接、IO设备。

    • 数据源分为:源设备、目标设备。

      1. 源设备:为程序提供数据,一般对应输入流。

      2. 目标设备:程序数据的目的地,一般对应输出流。

      图10-1 数据源示意图.png

  • 流的概念

    • 流是一个抽象、动态的概念,是一连串连续动态的数据集合。

    • 对于输入流而言,数据源就像水箱,流(stream)就像水管中流动着的水流,程序就是我们最终的用户。我们通过流(A Stream)将数据源(Source)中的数据(information)输送到程序(Program)中。

    • 对于输出流而言,目标数据源就是目的地(dest),我们通过流(A Stream)将程序(Program)中的数据(information)输送到目的数据源(dest)中。

      图10-2 流与源数据源和目标数据源之间的关系.png

  • 第一个简单的IO流程序及深入理解

    • 当程序需要读取数据源的数据时,就会通过IO流对象开启一个通向数据源的流,通过这个IO流对象的相关方法可以顺序读取数据源中的数据。

      import java.io.*;
      public class TestIO1 {
          public static void main(String[] args) {
              try {
                  //创建输入流
                  FileInputStream fis = new FileInputStream("d:/a.txt"); // 文件内容是:abc
                  //一个字节一个字节的读取数据
                  int s1 = fis.read(); // 打印输入字符a对应的ascii码值97
                  int s2 = fis.read(); // 打印输入字符b对应的ascii码值98
                  int s3 = fis.read(); // 打印输入字符c 对应的ascii码值99
                  int s4 = fis.read(); // 由于文件内容已经读取完毕,返回-1
                  System.out.println(s1);
                  System.out.println(s2);
                  System.out.println(s3);
                  System.out.println(s4);
                  // 流对象使用完,必须关闭!不然,总占用系统资源,最终会造成系统崩溃!
                  fis.close();
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }
      }
      
    • 注意:

      1. 在示例10-1中我们读取的文件内容是已知的,因此可以使用固定次数的“int s= fis.read();”语句读取内容,但是在实际开发中通常我们根本不知道文件的内容,因此我们在读取的时候需要配合while循环使用。

      2. 为了保证出现异常后流的正常关闭,通常要将流的关闭语句要放到finally语句块中,并且要判断流是不是null。

    • 使用流读取文件内容(经典代码,一定要掌握)

      import java.io.*;
      public class TestIO2 {
          public static void main(String[] args) {
              FileInputStream fis = null;
              try {
                  fis = new FileInputStream("d:/a.txt"); // 内容是:abc
                  StringBuilder sb = new StringBuilder();
                  int temp = 0;
                  //当temp等于-1时,表示已经到了文件结尾,停止读取
                  while ((temp = fis.read()) != -1) {
                      sb.append((char) temp);
                  }
                  System.out.println(sb);
              } catch (Exception e) {
                  e.printStackTrace();
              } finally {
                  try {
                      //这种写法,保证了即使遇到异常情况,也会关闭流对象。
                      if (fis != null) {
                          fis.close();
                      }
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
      
  • Java中流的概念细分

    • 按流的方向分类:

      1. 输入流:数据流向是数据源到程序(以InputStream、Reader结尾的流)。

      2. 输出流:数据流向是程序到目的地(以OutPutStream、Writer结尾的流)。

    • 按处理的数据单元分类:

      1. 字节流:以字节为单位获取数据,命名上以Stream结尾的流一般是字节流,如FileInputStream、FileOutputStream。

      2. 字符流:以字符为单位获取数据,命名上以Reader/Writer结尾的流一般是字符流,如FileReader、FileWriter。

    • 按处理对象不同分类:

      1. 节点流:可以直接从数据源或目的地读写数据,如FileInputStream、FileReader、DataInputStream等。

      2. 处理流:不直接连接到数据源或目的地,是”处理流的流”。通过对其他流的处理提高程序的性能,如BufferedInputStream、BufferedReader等。处理流也叫包装流。

      ​ 节点流处于IO操作的第一线,所有操作必须通过它们进行;处理流可以对节点流进行包装,提高性能或提高程序的灵活性。

      图10-6 节点流处理流示意图.png

  • Java中IO流类的体系

    图10-7 Java中的IO流体系.png

    1. InputStream/OutputStream:字节流的抽象类。

    2. Reader/Writer:字符流的抽象类。

    3. FileInputStream/FileOutputStream:节点流:以字节为单位直接操作“文件”。

    4. ByteArrayInputStream/ByteArrayOutputStream:节点流:以字节为单位直接操作“字节数组对象”。

    5. ObjectInputStream/ObjectOutputStream:处理流:以字节为单位直接操作“对象”。

    6. DataInputStream/DataOutputStream:处理流:以字节为单位直接操作“基本数据类型与字符串类型”。

    7. FileReader/FileWriter:节点流:以字符为单位直接操作“文本文件”(注意:只能读写文本文件)。

    8. BufferedReader/BufferedWriter:处理流:将Reader/Writer对象进行包装,增加缓存功能,提高读写效率。

    9. BufferedInputStream/BufferedOutputStream:处理流:将InputStream/OutputStream对象进行包装,增加缓存功能,提高 读写效率。

    10. InputStreamReader/OutputStreamWriter:处理流:将字节流对象转化成字符流对象。

    11. PrintStream:处理流:将OutputStream进行包装,可以方便地输出字符,更加灵活。

  • 四大IO抽象类

    • InputStream/OutputStream和Reader/writer类是所有IO流类的抽象父类,我们有必要简单了解一下这个四个抽象类的作用。然后,通过它们具体的子类熟悉相关的用法。

      img

    • IO文件字节输入流操作标准步骤

      1. 创建源
      2. 选择流
      3. 操作
      4. 释放
    • 字节输入流测试:

      import java.io.*;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: TestIO05.java
       * @time: 2019/10/17 16:39
       * @desc: 理解操作步骤
       */
      
      public class TestIO05 {
          public static void main(String[] args){
              // 1. 创建源
              File src = new File("abc.txt");
              // 2. 选择流
              try{
                  InputStream is = new FileInputStream(src);
                  System.out.println(src.getAbsolutePath());
                  // 3. 操作(读取)
                  int data1 = is.read();      // 第1个数据
                  int data2 = is.read();      // 第2个数据
                  int data3 = is.read();      // 第3个数据
                  System.out.println((char)data1);
                  System.out.println((char)data2);
                  System.out.println((char)data3);
                  // 4. 释放资源
                  is.close();
              }catch(FileNotFoundException e){
                  e.printStackTrace();
              }catch (IOException e){
                  e.printStackTrace();
              }
          }
      }
      
    • 字节输入流标准操作

      import java.io.*;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: TestIO06.java
       * @time: 2019/10/17 16:39
       * @desc: 理解操作步骤 标准
       */
      
      public class TestIO06 {
          public static void main(String[] args){
              // 1. 创建源
              File src = new File("abc.txt");
              // 2. 选择流
              InputStream is = null;
              try{
                  is = new FileInputStream(src);
                  // 3. 操作(读取)
                  int temp;
                  while((temp=is.read()) != -1){
                      System.out.println((char)temp);
                  }
              }catch(FileNotFoundException e){
                  e.printStackTrace();
              }catch (IOException e){
                  e.printStackTrace();
              }finally{
                  // 4. 释放资源
                  if (null != is) {
                      try {
                          is.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }
      }
      
    • 字节输出流标准操作

      import java.io.*;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: TestIO07.java
       * @time: 2019/10/17 16:39
       * @desc: 读入字符数组
       */
      
      public class TestIO07 {
          public static void main(String[] args){
              // 1. 创建源
              File src = new File("abc.txt");
              // 2. 选择流
              InputStream is = null;
              try{
                  is = new FileInputStream(src);
                  // 3. 操作(读取)
                  // 缓冲容器,这里设为3个字节
                  byte[] car = new byte[3];
                  // 接受长度
                  int len = -1;
                  while((len=is.read(car)) != -1){
                      // 字节数组 --> 字符串(解码)
                      String str = new String(car, 0, len);
                      System.out.println(str);
                  }
              }catch(FileNotFoundException e){
                  e.printStackTrace();
              }catch (IOException e){
                  e.printStackTrace();
              }finally{
                  // 4. 释放资源
                  if (null != is) {
                      try {
                          is.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }
      }
      
    • IO文件字节输出流操作标准步骤

      1. 创建源
      2. 选择流
      3. 操作(写出内容)
      4. 释放资源
    • 输出流实战

      import java.io.*;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: TestIO08.java
       * @time: 2019/10/17 18:10
       * @desc: 文件字节输出流
       */
      
      public class TestIO08 {
          public static void main(String[] args){
              // 1. 创建源
              File dest = new File("dest.txt");
              // 2. 选择流
              OutputStream os = null;
              try{
                  // true则是增加,false则是不增加
                  os = new FileOutputStream(dest, true);
                  // 3. 操作(写出)
                  String temp = "IO is so easy!";
                  byte[] datas = temp.getBytes();
                  os.write(datas, 0, datas.length);
                  os.flush();
              }catch(FileNotFoundException e){
                  e.printStackTrace();
              }catch (IOException e){
                  e.printStackTrace();
              }finally{
                  // 释放资源
                  if(null != os){
                      try {
                          os.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }
      }
      
    • 文件的拷贝

      import java.io.*;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: TestFileCopy.java
       * @time: 2019/10/17 18:22
       * @desc: 文件的拷贝
       */
      
      public class TestFileCopy {
          public static void main(String[] args) {
              copy("test.png", "copy_test.png");
          }
      
          public static void copy(String srcPath, String destPath){
              // 1. 创建源
              // 源头
              File src = new File(srcPath);
              File dest = new File(destPath);
              // 2. 选择流
              InputStream is = null;
              OutputStream os = null;
              try{
                  is = new FileInputStream(src);
                  os = new FileOutputStream(dest, true);
                  // 3. 操作(分段读取)
                  // 缓冲容器
                  byte[] flush = new byte[1024];
                  // 接受长度
                  int len = -1;
                  while((len=is.read(flush)) != -1){
                      // 字节数组 --> 字符串(解码)
                      String str = new String(flush, 0, len);
                      os.write(flush, 0, len);
                  }
                  os.flush();
              } catch (FileNotFoundException e) {
                  e.printStackTrace();
              } catch (IOException e) {
                  e.printStackTrace();
              } finally {
                  // 释放资源 先打开的后关闭
                  try{
                      if(null != os){
                          os.close();
                      }
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
      
                  try{
                      if(null != is){
                          is.close();
                      }
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
      
    • 字符输入流

      import java.io.*;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: TestIO07.java
       * @time: 2019/10/17 16:39
       * @desc: 文件字符输入流
       */
      
      public class TestIO09 {
          public static void main(String[] args){
              // 1. 创建源
              File src = new File("abc.txt");
              // 2. 选择流
              Reader reader = null;
              try{
                  reader = new FileReader(src);
                  // 3. 操作(读取)
                  char[] flush = new char[1024];
                  // 接受长度
                  int len = -1;
                  while((len=reader.read(flush)) != -1){
                      String str = new String(flush, 0, len);
                      System.out.println(str);
                  }
              }catch(FileNotFoundException e){
                  e.printStackTrace();
              }catch (IOException e){
                  e.printStackTrace();
              }finally{
                  // 4. 释放资源
                  if (null != reader) {
                      try {
                          reader.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }
      }
      
    • 字符输出流

      import java.io.*;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: TestIO08.java
       * @time: 2019/10/17 18:10
       * @desc: 文件字符输出流
       */
      
      public class TestIO10 {
          public static void main(String[] args){
              // 1. 创建源
              File dest = new File("dest.txt");
              // 2. 选择流
              Writer writer = null;
              try{
                  // true则是增加,false则是不增加
                  writer = new FileWriter(dest, false);
                  // 3. 操作(写出)
      
                  // 写法1
                  String temp = "IO is so easy!我是你大爷";
                  char[] datas = temp.toCharArray();
                  writer.write(datas, 0, datas.length);
                  writer.flush();
      
                  // 写法2
                  String temp = "IO is so easy!我是你大爷";
                  writer.write(temp);
                  writer.flush();
      
                  // 写法3
                  writer.append("IO is so easy!").append("我是你大爷");
                  writer.flush();
      
              }catch(FileNotFoundException e){
                  e.printStackTrace();
              }catch (IOException e){
                  e.printStackTrace();
              }finally{
                  // 释放资源
                  if(null != writer){
                      try {
                          writer.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }
      }
      
    • 字节数组输入流

      1. 创建源:字节数组不要太大
      2. 选择流
      3. 操作
      4. 释放资源:可以不做处理
      import java.io.*;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: TestIO07.java
       * @time: 2019/10/17 16:39
       * @desc: 字节数组输入流
       */
      
      public class TestIO11 {
          public static void main(String[] args){
              // 1. 创建源
              byte[] src = "talk is cheap show me the code. ".getBytes();
              // 2. 选择流
              InputStream is = null;
              try{
                  is = new ByteArrayInputStream(src);
                  // 3. 操作(读取)
                  // 缓冲容器,这里设为5个字节
                  byte[] car = new byte[5];
                  // 接受长度
                  int len = -1;
                  while((len=is.read(car)) != -1){
                      // 字节数组 --> 字符串(解码)
                      String str = new String(car, 0, len);
                      System.out.println(str);
                  }
              }catch (IOException e){
                  e.printStackTrace();
              }finally{
                  // 4. 释放资源
                  if (null != is) {
                      try {
                          is.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }
      }
      
    • 字节数组输出流:

      1. 创建源:内部维护
      2. 选择流:不关联源
      3. 操作:写出内容
      4. 获取数据:toByteArray()
      5. 释放资源:可以不用处理
      import java.io.*;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: TestIO08.java
       * @time: 2019/10/17 18:10
       * @desc: 字节数组输出流
       */
      
      public class TestIO12 {
          public static void main(String[] args){
              // 1. 创建源:不用创建源
              byte[] dest = null;
              // 2. 选择流:新增方法
              ByteArrayOutputStream baos = null;
              try{
                  // true则是增加,false则是不增加
                  baos = new ByteArrayOutputStream();
                  // 3. 操作(写出)
                  String temp = "show me the code bie bibi";
                  byte[] datas = temp.getBytes();
                  baos.write(datas, 0, datas.length);
                  baos.flush();
                  // 获取数据
                  dest = baos.toByteArray();
                  System.out.println(dest.length + "-->" + new String(dest, 0, baos.size()));
              } catch(IOException e){
                  e.printStackTrace();
              } finally{
                  // 释放资源
                  if(null != baos){
                      try {
                          baos.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }
      
          }
      }
      
    • 从文件拷贝到字节数组,再从字节数组输出到文件。

      import java.io.*;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: TestIO08.java
       * @time: 2019/10/17 18:10
       * @desc: 图片读取到字节数组,字节数组写出到文件
       */
      
      public class TestIO13 {
          public static void main(String[] args){
              byte[] datas = fileToByteArray("test.png");
              System.out.println(datas.length);
              byteArrayToFile(datas, "p-byte.png");
          }
      
          public static byte[] fileToByteArray(String filePath){
              /*
                1. 图片读取到字节数组中
                1). 图片到程序:FileInputStream
                2). 程序到字节数组:ByteArrayOutputStream
               */
      
              // 1. 创建源与目的地
              File src = new File(filePath);
              byte[] dest = null;
              // 2. 选择流
              InputStream is = null;
              ByteArrayOutputStream baos = null;
              try{
                  is = new FileInputStream(src);
                  baos = new ByteArrayOutputStream();
                  // 3. 操作:分段读取
                  byte[] flush = new byte[1024*10];
                  int len = -1;
                  while((len = is.read(flush)) != -1){
                      baos.write(flush, 0, len);      // 写出到字节数组中
                  }
                  baos.flush();
                  return baos.toByteArray();
              } catch (FileNotFoundException e) {
                  e.printStackTrace();
              } catch (IOException e) {
                  e.printStackTrace();
              }finally {
                  // 4. 释放资源
                  try{
                      if(null != is){
                          is.close();
                      }
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
              return null;
          }
      
          public static void byteArrayToFile(byte[] src, String filePath){
              /*
              2. 字节数组写出到文件
              1). 字节数组到程序:ByteArrayInputStream
              2). 程序写出到文件:FileOutputStream
               */
              // 1. 创建源
              File dest = new File(filePath);
              // 2. 选择流
              InputStream is = null;
              OutputStream os = null;
              try{
                  is = new ByteArrayInputStream(src);
                  os = new FileOutputStream(dest, false);
                  // 3. 操作:分段读取
                  byte[] flush = new byte[5];     // 缓冲容器
                  int len = -1;
                  while((len = is.read(flush)) != 1){
                      os.write(flush, 0, len);
                  }
                  os.flush();
              } catch (FileNotFoundException e) {
                  e.printStackTrace();
              } catch (IOException e) {
                  e.printStackTrace();
              } finally {
                  // 4. 释放资源
                  try {
                      if (null != os) {
                          os.close();
                      }
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
      
    • java1.7之后可以用try…with…resource自动释放

      try(is;os){}
      try(InputStream is = new FileInputStream(src); 
      OutputStream os = new FileOutputStream(dest);){}
      

2. IO的API

  • IO基础操作

    import java.io.File;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: TestIO1.java
     * @time: 2019/10/9 17:19
     * @desc: IO学习1
     */
    
    public class TestIO1 {
        public static void main(String[] args){
    
            // 输出文件分隔符
            System.out.println(File.separator);
    
            // 1. 构建File对象
            String path = "F:/BookStudy/else/Java知识点思维导图.png";
            File src = new File(path);
    
            // 输出文件大小
            System.out.println(src.length());
    
            // 2. 第二种构建File对象的方法
            File src2 = new File("F:/BookStudy/else", "Java知识点思维导图.png");
            System.out.println(src2.length());
    
            // 3. 第三种构建File对象的方法
            File src3 = new File(new File("F:/BookStudy/else"), "Java知识点思维导图.png");
            System.out.println(src3.length());
    
            // 相对路径的源路径
            System.out.println(System.getProperty("user.dir"));
    
            // 绝对路径
            System.out.println(src3.getAbsolutePath());
    
            // 构建一个不存在的对象
            File src4 = new File("aaa/asdf.jpg");
            System.out.println(src4.getAbsolutePath());
    
      }
    }
    
  • 文件操作

    import java.io.File;
    import java.io.IOException;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: TestIO2.java
     * @time: 2019/10/11 17:31
     * @desc: IO操作api
     */
    
    public class TestIO2 {
        public static void main(String[] args) throws IOException {
            File src = new File("F:/BookStudy/else/Java知识点思维导图.png");
    
            // 基本信息
            System.out.println("名称:" + src.getName());
            System.out.println("路径:" + src.getPath());
            System.out.println("绝对路径:" + src.getAbsolutePath());
            System.out.println("父路径:" + src.getParent());
            System.out.println("父对象:" + src.getParentFile().getName());
    
            // 文件状态
            System.out.println("是否存在:" + src.exists());
            System.out.println("是否文件:" + src.isFile());
            System.out.println("是否文件夹:" + src.isDirectory());
    
            // 获取文件的字节数,如果是文件夹,则为0。
            System.out.println("长度:" + src.length());
    
            // 创建文件:不存在才创建,返回true,不然返回false;不带后缀只是文件名,不是文件夹
            boolean flag = src.createNewFile();
            System.out.println(flag);
    
            // 文件的删除:删除已经存在的文件
            flag = src.delete();
            System.out.println(flag);
    
        }
    }
    
  • 文件夹的创建和遍历

    import java.io.File;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: TestIO3.java
     * @time: 2019/10/11 17:50
     * @desc: 文件夹创建和遍历
     */
    
    public class TestIO3 {
        public static void main(String[] args){
            // mkdir():确保上级目录存在,不存在则创建失败
            // mkdirs():上级目录可以不存在,不存在则一同创建
            File dir = new File("D:/");
    
            boolean flag1 = dir.mkdir();
            boolean flag2 = dir.mkdirs();
            System.out.println(flag1);
            System.out.println(flag2);
    
            // list():列出下级名称
            // listFiles():列出下级File对象
            String[] subNames = dir.list();
            for(String s: subNames){
                System.out.println(s);
            }
    
            File[] subFiles = dir.listFiles();
            for(File s: subFiles){
                System.out.println(s.getAbsolutePath());
            }
    
            // listRoots():列出所有盘符
            File[] roots = dir.listRoots();
            for(File r: roots){
                System.out.println(r.getAbsolutePath());
            }
    
            // 递归:方法自己调用自己
            // 递归头:何时结束递归
            // 递归体:重复调用
            printName(dir, 0);
    
        }
    
        public static void printName(File src, int deep){
            /* 打印子孙级目录和文件的名称 */
            for(int i=0; i<deep; i++){
                System.out.print("-");
            }
            System.out.println(src.getName());
            if(null == src || !src.exists()){
                return;
            } else if(src.isDirectory()){
                for(File s: src.listFiles()){
                    printName(s, deep + 1);
                }
            }
        }
    }
    
  • 统计文件夹的大小

    import java.io.File;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: TestIO4.java
     * @time: 2019/10/15 15:20
     * @desc: 统计文件夹的大小
     */
    
    public class TestIO4 {
        public static void main(String[] args){
            File src = new File("F:\\BookStudy");
            count(src);
            System.out.println(LEN);
        }
    
        private static long LEN = 0;
        public static void count(File src){
            // 获取大小
            if(null != src && src.exists()){
                if(src.isFile()){
                    LEN += src.length();
                }else{
                    for(File s: src.listFiles()){
                        count(s);
                    }
                }
            }
        }
    }
    
  • 使用面向对象:统计文件夹大小

    import java.io.File;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: DirCount.java
     * @time: 2019/10/15 15:58
     * @desc: 使用面向对象:统计文件夹大小
     */
    
    public class DirCount {
        // 大小
        private long len;
        // 文件夹路径
        private String path;
        // 源
        private File src;
        // 文件的个数
        private int fileSize;
        // 文件夹的个数
        private int dirSize;
    
        public DirCount(String path){
            this.path = path;
            this.src = new File(path);
            count(this.src);
        }
    
        private void count(File src){
            // 获取大小
            if(null != src && src.exists()){
                if(src.isFile()){
                    this.len += src.length();
                    this.fileSize++;
                }else{
                    this.dirSize++;
                    for(File s: src.listFiles()){
                        count(s);
                    }
                }
            }
        }
    
        public long getLen() {
            return len;
        }
    
        public int getFileSize() {
            return fileSize;
        }
    
        public int getDirSize() {
            return dirSize;
        }
    
        public static void main(String[] args){
            DirCount dir = new DirCount("F:\\BookStudy");
            System.out.println(dir.getLen());
            System.out.println("文件的数量" + "--->" + dir.getFileSize());
            System.out.println("文件夹的数量" + "--->" + dir.getDirSize());
    
    
            DirCount dir2 = new DirCount("F:\\BookStudy\\else");
            System.out.println(dir2.getLen());
            System.out.println("文件的数量" + "--->" + dir2.getFileSize());
            System.out.println("文件夹的数量" + "--->" + dir2.getDirSize());
    
        }
    }
    
  • 编码和解码

    import java.io.UnsupportedEncodingException;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: ContentEncode.java
     * @time: 2019/10/15 16:26
     * @desc: 编码:字符串-->字节;解码:字节-->字符串
     */
    
    public class ContentEncode {
        public static void main(String[] args) throws UnsupportedEncodingException {
            String msg = "你怕不是个铁憨憨";
            // 编码:字节数组
            byte[] datas = msg.getBytes();
            System.out.println(datas);
            // 中文utf-8:一个字符占3个字节;默认使用工程的字符集
            System.out.println(datas.length);
    
            // 编码:其他字符集
            datas = msg.getBytes("UTF-16LE");
            System.out.println(datas.length);
            datas = msg.getBytes("GBK");
            System.out.println(datas.length);
    
            // 解码
            msg = new String(datas, 0, datas.length, "gbk");
            System.out.println(msg);
        }
    }
    
  • 乱码的原因:

    1. 字节数不够
    2. 字符集不统一

3. 装饰流

  • 装饰器模式原理剖析

    1. 抽象组件:需要装饰的抽象对象(接口或抽象父类)
    2. 具体组件:需要装饰的对象
    3. 抽象装饰类:包含了对抽象组件的引用以及装饰者共有的方法
    4. 具体装饰类:被装饰的对象
    • 模拟声音

      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: DecorateTest01.java
       * @time: 2019/10/18 15:53
       * @desc: 实现放大器对声音放大的功能
       */
      
      public class DecorateTest01 {
          public static void main(String[] args){
              Person p = new Person();
              p.say();
      
              // 装饰
              Amplifier am = new Amplifier(p);
              am.say();
          }
      }
      
      interface Say{
          void say();
      }
      
      class Person implements Say{
          // 属性
          private int voice = 10;
      
          @Override
          public void say() {
              System.out.println("人的声音为:" + this.getVoice());
          }
      
          public int getVoice() {
              return voice;
          }
      
          public void setVoice(int voice) {
              this.voice = voice;
          }
      }
      
      class Amplifier implements Say{
          private Person p;
          Amplifier(Person p){
              this.p = p;
          }
      
          @Override
          public void say() {
              System.out.println("人的声音为:" + p.getVoice()*100);
              System.out.println("噪音...");
          }
      }
      
    • 模拟咖啡

      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: DecorateTest02.java
       * @time: 2019/10/18 15:53
       * @desc: 模拟咖啡
       */
      
      public class DecorateTest02 {
          public static void main(String[] args){
              Drink coffee = new Coffee();
              Drink suger = new Suger(coffee);    // 装饰
              System.out.println(suger.info() + "-->" + suger.cost());
              Drink milk = new Milk(coffee);    // 装饰
              System.out.println(milk.info() + "-->" + milk.cost());
      
              Drink mixed = new Milk(suger);    // 装饰
              System.out.println(mixed.info() + "-->" + mixed.cost());
          }
      }
      
      // 抽象组件
      interface Drink{
          double cost();      // 费用
          String info();      // 说明
      }
      
      // 具体组件
      class Coffee implements Drink{
          private String name = "原味咖啡";
      
          @Override
          public double cost() {
              return 10;
          }
      
          @Override
          public String info() {
              return name;
          }
      }
      
      // 抽象装饰类
      abstract class Decorate implements Drink{
          // 对抽象组件的引用
          private Drink drink;
          public Decorate(Drink drink){
              this.drink = drink;
          }
      
          @Override
          public double cost() {
              return this.drink.cost();
          }
      
          @Override
          public String info() {
              return this.drink.info();
          }
      }
      
      // 具体装饰类
      class Milk extends Decorate{
          public Milk(Drink drink) {
              super(drink);
          }
      
          @Override
          public double cost() {
              return super.cost()*4;
          }
      
          @Override
          public String info() {
              return super.info() + "加入了牛奶";
          }
      }
      
      class Suger extends Decorate{
          public Suger(Drink drink) {
              super(drink);
          }
      
          @Override
          public double cost() {
              return super.cost()*2;
          }
      
          @Override
          public String info() {
              return super.info() + "加入了糖";
          }
      }
      
  • 字节缓冲流(输入输出同理)

    import java.io.*;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: TestIO07.java
     * @time: 2019/10/17 16:39
     * @desc: 加入缓冲流(只需要释放底层的is)
     */
    
    public class BufferedTest02 {
        public static void main(String[] args){
            // 1. 创建源
            File src = new File("abc.txt");
            // 2. 选择流
            InputStream is = null;
            try{
                is = new BufferedInputStream(new FileInputStream(src));
                // 3. 操作(读取)
                byte[] flush = new byte[1024];
                // 接受长度
                int len = -1;
                while((len=is.read(flush)) != -1){
                    // 字节数组 --> 字符串(解码)
                    String str = new String(flush, 0, len);
                    System.out.println(str);
                }
            }catch(FileNotFoundException e){
                e.printStackTrace();
            }catch (IOException e){
                e.printStackTrace();
            }finally{
                // 4. 释放资源
                if (null != is) {
                    try {
                        is.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    
  • 字符缓冲流(输入输出同理)

    import java.io.*;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: TestIO07.java
     * @time: 2019/10/17 16:39
     * @desc: 文件字符输入流 加入缓冲流
     */
    
    public class BufferedTest03 {
        public static void main(String[] args){
            // 1. 创建源
            File src = new File("abc.txt");
            // 2. 选择流
            BufferedReader reader = null;
            try{
                reader = new BufferedReader(new FileReader(src));
                // 3. 操作(读取)
                String line = null;
                while((line = reader.readLine()) != null){
                    System.out.println(line);
                }
            }catch(FileNotFoundException e){
                e.printStackTrace();
            }catch (IOException e){
                e.printStackTrace();
            }finally{
                // 4. 释放资源
                if (null != reader) {
                    try {
                        reader.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    
  • 转换流:InputStreamReader OutputStreamWriter

    1. 以字符流的形式操作字节流(纯文本的)
    2. 指定字符集
    • 循环读取键盘输入
    import java.io.*;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: ConvertTest1.java
     * @time: 2019/10/19 14:48
     * @desc: 转换流:InputStreamReader OutputStreamWriter
     */
    
    public class ConvertTest1 {
        public static void main(String[] args){
            // 操作System.in和System.out
            try(BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));){
                // 循环获取键盘的输入(exit退出),输入此内容
                String msg = "";
                while(!msg.equals("exit")){
                    msg = reader.readLine();        // 循环读取
                    writer.write(msg);              // 循环写出
                    writer.newLine();
                    writer.flush();                 // 强制刷新
                }
            } catch (IOException e) {
                System.out.println("操作异常");
            }
        }
    }
    
    • 读取网站的内容并保存
    import java.io.*;
    import java.net.URL;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: ConvertTest2.java
     * @time: 2019/10/19 14:48
     * @desc: 转换流:InputStreamReader OutputStreamWriter
     */
    
    public class ConvertTest2 {
        public static void main(String[] args) {
            // 中文乱码
            test1();
            // 中文不是乱码
            test2();
            // 效率更高
            test3();
        }
        public static void test1(){
            // 操作网络流,下载百度的源代码
            try(InputStream is = new URL("http://www.baidu.com").openStream();){
                int temp;
                while((temp=is.read()) != -1){
                    System.out.print((char)temp);
                }
            } catch (IOException e) {
                System.out.println("操作异常");
            }
        }
        public static void test2(){
            // 操作网络流,下载百度的源代码
            try(InputStreamReader is = new InputStreamReader(new URL("http://www.baidu.com").openStream(), "UTF-8")){
                int temp;
                while((temp=is.read()) != -1){
                    System.out.print((char)temp);
                }
            } catch (IOException e) {
                System.out.println("操作异常");
            }
        }
        public static void test3(){
            // 操作网络流,下载百度的源代码
            try(BufferedReader reader =
                        new BufferedReader(
                                new InputStreamReader(
                                        new URL("http://www.baidu.com").openStream(), "UTF-8"
                                )
                        );
                BufferedWriter writer =
                        new BufferedWriter(
                                new OutputStreamWriter(
                                        new FileOutputStream("baidu.html"), "UTF-8"
                                )
                        )
            ){
                String msg;
                while((msg = reader.readLine()) != null){
                    writer.write(msg);      // 字符集不统一,字节数不够出现乱码
                    writer.newLine();
                }
            } catch (IOException e) {
                System.out.println("操作异常");
            }
        }
    }
    
  • 数据流

    1. 先写出后读取
    2. 读取的顺序与写出保持一致
    import com.sun.xml.internal.messaging.saaj.util.ByteInputStream;
    
    import java.io.*;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: DataTest1.java
     * @time: 2019/10/19 15:57
     * @desc: 数据流
     */
    
    public class DataTest1 {
        public static void main(String[] args) throws IOException {
            // 写出
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            DataOutputStream dos = new DataOutputStream(
                    new BufferedOutputStream(baos)
            );
            // 操作数据类型 + 数据
            dos.writeUTF("编码辛酸泪啊");
            dos.writeInt(18);
            dos.writeBoolean(false);
            dos.writeChar('a');
            dos.flush();
    
            byte[] datas = baos.toByteArray();
            System.out.println(datas.length);
    
            // 读取
            DataInputStream dis = new DataInputStream(
                    new BufferedInputStream(
                        new ByteArrayInputStream(datas)
                    )
            );
            // 顺序与写出一致
            String msg = dis.readUTF();
            int age = dis.readInt();
            boolean flag = dis.readBoolean();
            char ch = dis.readChar();
            System.out.println(flag);
        }
    }
    
  • 对象流

    1. 先写出后读取
    2. 读取的顺序与写出保持一致
    3. 不是所有的对象都可以序列化,必须要实现接口Serializable
    • 将对象数据序列化并反序列化

      import java.io.*;
      import java.util.Date;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: DataTest1.java
       * @time: 2019/10/19 15:57
       * @desc: 对象流
       */
      
      public class ObjectTest1 {
          public static void main(String[] args) throws IOException, ClassNotFoundException {
              // 写出:序列化
              ByteArrayOutputStream baos = new ByteArrayOutputStream();
              ObjectOutputStream dos = new ObjectOutputStream(
                      new BufferedOutputStream(baos)
              );
              // 操作数据类型 + 数据
              dos.writeUTF("编码辛酸泪");
              dos.writeInt(18);
              dos.writeBoolean(false);
              dos.writeChar('a');
              // 对象
              dos.writeObject("谁解其中味");
              dos.writeObject(new Date());
              Employee emp = new Employee("马云", 400);
              dos.writeObject(emp);
              dos.flush();
      
              byte[] datas = baos.toByteArray();
              System.out.println(datas.length);
      
              // 读取:反序列化
              ObjectInputStream dis = new ObjectInputStream(
                      new BufferedInputStream(
                          new ByteArrayInputStream(datas)
                      )
              );
              // 顺序与写出一致
              String msg = dis.readUTF();
              int age = dis.readInt();
              boolean flag = dis.readBoolean();
              char ch = dis.readChar();
              System.out.println(flag);
              // 对象的数据还原
              Object str = dis.readObject();
              Object date = dis.readObject();
              Object employee = dis.readObject();
      
              if (str instanceof String){
                  String strObj = (String) str;
                  System.out.println(strObj);
              }
              if (date instanceof Date){
                  Date strObj = (Date) date;
                  System.out.println(strObj);
              }
              if (employee instanceof Employee){
                  Employee strObj = (Employee) employee;
                  System.out.println(strObj.getName() + "-->" + strObj.getSalary());
              }
          }
      }
      
      // javabean 封装数据
      class Employee implements java.io.Serializable{
          private transient String name;      // 该数据不需要序列化
          private double salary;
      
          public Employee() {
          }
      
          public Employee(String name, double salary) {
              this.name = name;
              this.salary = salary;
          }
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          public double getSalary() {
              return salary;
          }
      
          public void setSalary(double salary) {
              this.salary = salary;
          }
      }
      
    • 在上面的基础上,序列化成文件并反序列化

      import java.io.*;
      import java.util.Date;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: DataTest2.java
       * @time: 2019/10/19 15:57
       * @desc: 对象流
       */
      
      public class ObjectTest2 {
          public static void main(String[] args) throws IOException, ClassNotFoundException {
              // 写出:序列化
              ObjectOutputStream oos = new ObjectOutputStream(
                      new BufferedOutputStream(
                              new FileOutputStream("obj.txt")
                      )
              );
              // 操作数据类型 + 数据
              oos.writeUTF("编码辛酸泪");
              oos.writeInt(18);
              oos.writeBoolean(false);
              oos.writeChar('a');
              // 对象
              oos.writeObject("谁解其中味");
              oos.writeObject(new Date());
              Employee emp = new Employee("马云", 400);
              oos.writeObject(emp);
              oos.flush();
              oos.close();
      
              // 读取:反序列化
              ObjectInputStream ois = new ObjectInputStream(
                      new BufferedInputStream(
                          new FileInputStream("obj.txt")
                      )
              );
              // 顺序与写出一致
              String msg = ois.readUTF();
              int age = ois.readInt();
              boolean flag = ois.readBoolean();
              char ch = ois.readChar();
              System.out.println(flag);
              // 对象的数据还原
              Object str = ois.readObject();
              Object date = ois.readObject();
              Object employee = ois.readObject();
      
              if (str instanceof String){
                  String strObj = (String) str;
                  System.out.println(strObj);
              }
              if (date instanceof Date){
                  Date strObj = (Date) date;
                  System.out.println(strObj);
              }
              if (employee instanceof Employee){
                  Employee strObj = (Employee) employee;
                   System.out.println(strObj.getName() + "-->" + strObj.getSalary());
              }
          }
      }
      
  • 打印流

    • PrintStream

      import java.io.*;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: PrintTest1.java
       * @time: 2019/10/20 15:43
       * @desc: 打印流
       */
      
      public class PrintTest1 {
          public static void main(String[] args) throws FileNotFoundException {
              // 打印流System.out
              PrintStream ps = System.out;
              ps.println("打印流");
              ps.println(true);
      
              ps = new PrintStream(
                      new BufferedOutputStream(
                              new FileOutputStream("print.txt")
                      ), true
              );
              ps.println("打印流");
              ps.println(true);
              ps.close();
      
              // 重定向输出端
              System.setOut(ps);
              System.out.println("change");
              // 重定向回控制台
              System.setOut(
                      new PrintStream(
                              new BufferedOutputStream(
                                      new FileOutputStream(FileDescriptor.out)
                              ), true
                      )
              );
              System.out.println("i am backing...");
          }
      }
      
    • PrintWriter

      import java.io.*;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: PrintTest2.java
       * @time: 2019/10/20 15:43
       * @desc: 打印流
       */
      
      public class PrintTest2 {
          public static void main(String[] args) throws FileNotFoundException {
      
              PrintWriter pw = new PrintWriter(
                      new BufferedOutputStream(
                              new FileOutputStream("print.txt")
                      ), true
              );
              pw.println("打印流");
              pw.println(true);
              pw.close();
          }
      }
      

4. IO实战

  • 文件分割

    • 随机读取和写入流

      import java.io.File;
      import java.io.FileNotFoundException;
      import java.io.IOException;
      import java.io.RandomAccessFile;
      import java.util.Random;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: RanTest1.java
       * @time: 2019/10/21 9:01
       * @desc: 随机读取和写入流 RandomAccessFile
       */
      
      public class RanTest1 {
          public static void main(String[] args) throws IOException {
              // 分多少块
              File src = new File("D:\\李添的数据哦!!!\\BookStudy\\else\\JAVAPro\\src\\PrintTest2.java");
              // 总长度
              long len = src.length();
              // 每块大小
              int blockSize = 240;
              // 块数:多少块
              int size = (int)Math.ceil(len*1.0/blockSize);
              System.out.println(size);
              int beginPos = 0;
              int actualSize = (int)(blockSize>len?len:blockSize);
      
              for(int i=0; i<size; i++){
                  beginPos = i*blockSize;
                  if(i == size-1){
                      // 最后一块
                      actualSize = (int)len;
                  }else{
                      actualSize = blockSize;
                      // 剩余量
                      len -= actualSize;
                  }
                  System.out.println(i + "-->" + beginPos + "-->" + actualSize);
                  test1(i, beginPos, actualSize);
              }
          }
      
          // 指定起始位置,读取剩余指定长度内容
          public static void test1(int i, int beginPos, int actualSize) throws IOException {
              RandomAccessFile raf = new RandomAccessFile(new File("D:\\李添的数据哦!!!\\BookStudy\\else\\JAVAPro\\src\\PrintTest2.java"), "r");
              // 指定起始位置
      //        int beginPos = 2;
              // 实际大小
      //        int actualSize = 128;
              // 随机读取
              raf.seek(beginPos);
              byte[] flush = new byte[124];
              // 接受长度
              int len = -1;
              while((len = raf.read(flush)) != -1){
                  if (actualSize > len){
                      // 实际大小大于接受长度,则获取本次读取的所有内容
                      System.out.println(new String(flush, 0, len));
                      actualSize -= len;
                  }else{
                      System.out.println(new String(flush, 0, actualSize));
                      break;
                  }
              }
      
              raf.close();
          }
      }
      
    • 增加输出流

      import java.io.File;
      import java.io.IOException;
      import java.io.RandomAccessFile;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: RanTest1.java
       * @time: 2019/10/21 9:01
       * @desc: 随机读取和写入流 RandomAccessFile 并增加输出流
       */
      
      public class RanTest2 {
          public static void main(String[] args) throws IOException {
              // 分多少块
              File src = new File("D:\\李添的数据哦!!!\\BookStudy\\else\\JAVAPro\\src\\PrintTest2.java");
              // 总长度
              long len = src.length();
              // 每块大小
              int blockSize = 240;
              // 块数:多少块
              int size = (int)Math.ceil(len*1.0/blockSize);
              System.out.println(size);
              int beginPos = 0;
              int actualSize = (int)(blockSize>len?len:blockSize);
      
              for(int i=0; i<size; i++){
                  beginPos = i*blockSize;
                  if(i == size-1){
                      // 最后一块
                      actualSize = (int)len;
                  }else{
                      actualSize = blockSize;
                      // 剩余量
                      len -= actualSize;
                  }
                  System.out.println(i + "-->" + beginPos + "-->" + actualSize);
                  test1(i, beginPos, actualSize);
              }
          }
      
          // 指定起始位置,读取剩余指定长度内容
          public static void test1(int i, int beginPos, int actualSize) throws IOException {
              RandomAccessFile raf = new RandomAccessFile(new File("D:\\李添的数据哦!!!\\BookStudy\\else\\JAVAPro\\src\\PrintTest2.java"), "r");
              RandomAccessFile raf2 = new RandomAccessFile(new File("Print_Copy_" + i + ".java"), "rw");
              // 指定起始位置
      //        int beginPos = 2;
              // 实际大小
      //        int actualSize = 128;
              // 随机读取
              raf.seek(beginPos);
              byte[] flush = new byte[124];
              // 接受长度
              int len = -1;
              while((len = raf.read(flush)) != -1){
                  if (actualSize > len){
                      // 实际大小大于接受长度,则获取本次读取的所有内容
                      raf2.write(flush, 0, len);
                      actualSize -= len;
                  }else{
                      raf2.write(flush, 0, actualSize);
                      break;
                  }
              }
              raf2.close();
              raf.close();
          }
      }
      
    • 对RanTest进行封装,功能是拆分文件,面向对象思想封装

      import java.io.File;
      import java.io.IOException;
      import java.io.RandomAccessFile;
      import java.util.ArrayList;
      import java.util.List;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: SplitFile.java
       * @time: 2019/10/21 9:35
       * @desc: 对RanTest进行封装,功能是拆分文件,面向对象思想封装
       */
      
      public class SplitFile {
          // 源头
          private File src;
          // 目的地(文件夹)
          private String destDir;
          // 所有分割后的文件存储路径
          private List<String> destPaths;
          // 每块大小
          private int blockSize;
          // 块数:多少块
          private int size;
      
          public SplitFile(String srcPath, String destDir, int blockSize){
              this.src = new File(srcPath);
              this.destDir = destDir;
              this.blockSize = blockSize;
              this.destPaths = new ArrayList<>();
      
              // 初始化
              init();
          }
      
          // 初始化
          private void init(){
              // 总长度
              long len = this.src.length();
              // 块数:多少块
              this.size = (int)Math.ceil(len*1.0/blockSize);
              // 路径
              for(int i=0; i<size; i++){
                  this.destPaths.add(this.destDir + "/" + i + "-" + this.src.getName());
              }
      
          }
      
          // 分割
          public void split() throws IOException {
              /*
              1. 计算每一块起始位置及大小
              2. 分割
               */
              // 总长度
              long len = this.src.length();
              // 每块大小
              int size = (int)Math.ceil(len*1.0/blockSize);
              System.out.println(size);
              int beginPos = 0;
              int actualSize = (int)(this.blockSize>len?len:this.blockSize);
      
              for(int i=0; i<size; i++){
                  beginPos = i*blockSize;
                  if(i == size-1){
                      // 最后一块
                      actualSize = (int)len;
                  }else{
                      actualSize = blockSize;
                      // 剩余量
                      len -= actualSize;
                  }
                  splitDetail(i, beginPos, actualSize);
              }
          }
      
          // 指定起始位置,读取剩余指定长度内容
          private void splitDetail(int i, int beginPos, int actualSize) throws IOException {
              RandomAccessFile raf = new RandomAccessFile((this.src), "r");
              RandomAccessFile raf2 = new RandomAccessFile((this.destPaths.get(i)), "rw");
              raf.seek(beginPos);
              byte[] flush = new byte[124];
              // 接受长度
              int len = -1;
              while((len = raf.read(flush)) != -1){
                  if (actualSize > len){
                      // 实际大小大于接受长度,则获取本次读取的所有内容
                      raf2.write(flush, 0, len);
                      actualSize -= len;
                  }else{
                      raf2.write(flush, 0, actualSize);
                      break;
                  }
              }
              raf2.close();
              raf.close();
          }
      
          public static void main(String[] args) throws IOException {
              SplitFile sf = new SplitFile("test.png", "dest", 1024*10);
              sf.split();
          }
      }
      
  • 增加文件的合并功能

    import java.io.*;
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: SplitFile.java
     * @time: 2019/10/21 9:35
     * @desc: 对RanTest进行封装,功能是拆分文件,面向对象思想封装
     */
    
    public class SplitFile {
        // 源头
        private File src;
        // 目的地(文件夹)
        private String destDir;
        // 所有分割后的文件存储路径
        private List<String> destPaths;
        // 每块大小
        private int blockSize;
        // 块数:多少块
        private int size;
    
        public SplitFile(String srcPath, String destDir, int blockSize){
            this.src = new File(srcPath);
            this.destDir = destDir;
            this.blockSize = blockSize;
            this.destPaths = new ArrayList<>();
    
            // 初始化
            init();
        }
    
        // 初始化
        private void init(){
            // 总长度
            long len = this.src.length();
            // 块数:多少块
            this.size = (int)Math.ceil(len*1.0/blockSize);
            // 路径
            for(int i=0; i<size; i++){
                this.destPaths.add(this.destDir + "/" + i + "-" + this.src.getName());
            }
    
        }
    
        // 分割
        public void split() throws IOException {
            /*
            1. 计算每一块起始位置及大小
            2. 分割
             */
            // 总长度
            long len = this.src.length();
            // 每块大小
            int size = (int)Math.ceil(len*1.0/blockSize);
            System.out.println(size);
            int beginPos = 0;
            int actualSize = (int)(this.blockSize>len?len:this.blockSize);
    
            for(int i=0; i<size; i++){
                beginPos = i*blockSize;
                if(i == size-1){
                    // 最后一块
                    actualSize = (int)len;
                }else{
                    actualSize = blockSize;
                    // 剩余量
                    len -= actualSize;
                }
                splitDetail(i, beginPos, actualSize);
            }
        }
    
        // 指定起始位置,读取剩余指定长度内容
        private void splitDetail(int i, int beginPos, int actualSize) throws IOException {
            RandomAccessFile raf = new RandomAccessFile((this.src), "r");
            RandomAccessFile raf2 = new RandomAccessFile((this.destPaths.get(i)), "rw");
            raf.seek(beginPos);
            byte[] flush = new byte[124];
            // 接受长度
            int len = -1;
            while((len = raf.read(flush)) != -1){
                if (actualSize > len){
                    // 实际大小大于接受长度,则获取本次读取的所有内容
                    raf2.write(flush, 0, len);
                    actualSize -= len;
                }else{
                    raf2.write(flush, 0, actualSize);
                    break;
                }
            }
            raf2.close();
            raf.close();
        }
    
        // 文件的合并
        private void merge(String destPath) throws IOException {
            // 输出流
            OutputStream os = new BufferedOutputStream(
                    new FileOutputStream(destPath, true)
            );
            // 输入流
            for (int i = 0; i < destPaths.size(); i++) {
                InputStream is = new BufferedInputStream((new FileInputStream(destPaths.get(i))));
                // 拷贝
                byte[] flush = new byte[1024];
                int len = -1;
                while((len = is.read(flush)) != -1){
                    os.write(flush, 0, len);
                }
                os.flush();
                is.close();
            }
            os.close();
        }
    
        public static void main(String[] args) throws IOException {
            SplitFile sf = new SplitFile("test.png", "dest", 1024*10);
            sf.split();
            sf.merge("merge.png");
        }
    }
    
  • 利用SequenceInputStream增加文件合并功能

    import java.io.*;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Vector;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: SplitFile.java
     * @time: 2019/10/21 9:35
     * @desc: 对RanTest进行封装,功能是拆分文件,面向对象思想封装
     */
    
    public class SplitFile {
        // 源头
        private File src;
        // 目的地(文件夹)
        private String destDir;
        // 所有分割后的文件存储路径
        private List<String> destPaths;
        // 每块大小
        private int blockSize;
        // 块数:多少块
        private int size;
    
        public SplitFile(String srcPath, String destDir, int blockSize){
            this.src = new File(srcPath);
            this.destDir = destDir;
            this.blockSize = blockSize;
            this.destPaths = new ArrayList<>();
    
            // 初始化
            init();
        }
    
        // 初始化
        private void init(){
            // 总长度
            long len = this.src.length();
            // 块数:多少块
            this.size = (int)Math.ceil(len*1.0/blockSize);
            // 路径
            for(int i=0; i<size; i++){
                this.destPaths.add(this.destDir + "/" + i + "-" + this.src.getName());
            }
    
        }
    
        // 分割
        public void split() throws IOException {
            /*
            1. 计算每一块起始位置及大小
            2. 分割
             */
            // 总长度
            long len = this.src.length();
            // 每块大小
            int size = (int)Math.ceil(len*1.0/blockSize);
            System.out.println(size);
            int beginPos = 0;
            int actualSize = (int)(this.blockSize>len?len:this.blockSize);
    
            for(int i=0; i<size; i++){
                beginPos = i*blockSize;
                if(i == size-1){
                    // 最后一块
                    actualSize = (int)len;
                }else{
                    actualSize = blockSize;
                    // 剩余量
                    len -= actualSize;
                }
                splitDetail(i, beginPos, actualSize);
            }
        }
    
        // 指定起始位置,读取剩余指定长度内容
        private void splitDetail(int i, int beginPos, int actualSize) throws IOException {
            RandomAccessFile raf = new RandomAccessFile((this.src), "r");
            RandomAccessFile raf2 = new RandomAccessFile((this.destPaths.get(i)), "rw");
            raf.seek(beginPos);
            byte[] flush = new byte[124];
            // 接受长度
            int len = -1;
            while((len = raf.read(flush)) != -1){
                if (actualSize > len){
                    // 实际大小大于接受长度,则获取本次读取的所有内容
                    raf2.write(flush, 0, len);
                    actualSize -= len;
                }else{
                    raf2.write(flush, 0, actualSize);
                    break;
                }
            }
            raf2.close();
            raf.close();
        }
    
        // 文件的合并
        private void merge(String destPath) throws IOException {
            // 输出流
            OutputStream os = new BufferedOutputStream(
                    new FileOutputStream(destPath, true)
            );
            // 输入流
            for (int i = 0; i < destPaths.size(); i++) {
                InputStream is = new BufferedInputStream((new FileInputStream(destPaths.get(i))));
                // 拷贝
                byte[] flush = new byte[1024];
                int len = -1;
                while((len = is.read(flush)) != -1){
                    os.write(flush, 0, len);
                }
                os.flush();
                is.close();
            }
            os.close();
        }
    
        // 利用合并流来进行文件的合并
        private void seq_merge(String destPath) throws IOException {
            // 输出流
            OutputStream os = new BufferedOutputStream(
                    new FileOutputStream(destPath, true)
            );
            Vector<InputStream> vi = new Vector<InputStream>();
            SequenceInputStream sis = null;
            // 输入流
            for (int i = 0; i < destPaths.size(); i++) {
                InputStream is = new BufferedInputStream((new FileInputStream(destPaths.get(i))));
            }
            sis = new SequenceInputStream(vi.elements());
    
            // 拷贝
            byte[] flush = new byte[1024];
            int len = -1;
            while((len = sis.read(flush)) != -1){
                os.write(flush, 0, len);
            }
            os.flush();
            sis.close();
            os.close();
        }
    
        public static void main(String[] args) throws IOException {
            SplitFile sf = new SplitFile("test.png", "dest", 1024*10);
            sf.split();
            sf.seq_merge("merge-seq.png");
        }
    }
    

5. CommonsIO

  • 常用核心操作和拷贝核心操作

    import org.apache.commons.io.FileUtils;
    import org.apache.commons.io.IOUtils;
    import org.apache.commons.io.LineIterator;
    import org.apache.commons.io.filefilter.DirectoryFileFilter;
    import org.apache.commons.io.filefilter.EmptyFileFilter;
    import org.apache.commons.io.filefilter.FileFilterUtils;
    import org.apache.commons.io.filefilter.SuffixFileFilter;
    
    import javax.imageio.stream.FileCacheImageInputStream;
    import java.io.File;
    import java.io.IOException;
    import java.net.URL;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: CIOTest1.java
     * @time: 2019/10/22 16:00
     * @desc:
     */
    
    public class CIOTest1 {
        public static void main(String[] args) throws IOException {
            // 文件大小
            long len = FileUtils.sizeOf(new File("D:\\李添的数据哦!!!\\BookStudy\\else\\JAVAPro\\src\\CIOTest1.java"));
            System.out.println(len);
    
            // 目录大小
            len = FileUtils.sizeOf(new File("D:\\李添的数据哦!!!\\BookStudy"));
            System.out.println(len);
    
            // 列出子孙集
            /*
            第一个参数:目标路径
            第二个参数:过滤文件:
                NOT_EMPTY,即只要非空文件
                SuffixFileFilter,即只要该后缀名的文件
            第三个参数:过滤目录:
                INSTANCE,即只看子孙集
             */
            Collection<File> files = FileUtils.listFiles(
                    new File("D:\\李添的数据哦!!!\\BookStudy\\else\\JAVAPro"),
                    FileFilterUtils.or(EmptyFileFilter.NOT_EMPTY, new SuffixFileFilter("java"), new SuffixFileFilter("class")), DirectoryFileFilter.INSTANCE
            );
            for (File file : files) {
                System.out.println(file.getAbsolutePath());
            }
    
            // 读取文件内容
            String path = "D:\\李添的数据哦!!!\\BookStudy\\else\\【参考】3. 代码快捷键操作.md";
            String msg = FileUtils.readFileToString(new File(path), "UTF-8");
            System.out.println(msg);
            byte[] datas = FileUtils.readFileToByteArray(new File(path));
            System.out.println(datas.length);
    
            // 逐行读取
            List<String> msgs = FileUtils.readLines(new File((path)), "UTF-8");
            for (String str : msgs) {
                System.out.println(str);
            }
            // 逐行读取2
            LineIterator it = FileUtils.lineIterator(new File(path), "UTF-8");
            while (it.hasNext()) {
                System.out.println(it.nextLine());
            }
    
            // 写出内容到文件
            FileUtils.write(new File("happy.txt"), "学习是一件伟大的事业\n", "UTF-8");
            FileUtils.writeStringToFile(new File("happy.txt"), "学习是一件辛苦的事业\n", "UTF-8", true);
            FileUtils.writeByteArrayToFile(new File("happy.txt"), "学习是一件快乐的事业\n".getBytes("UTF-8"), true);
    
            // 写出列表
            List<String> dd = new ArrayList<>();
            dd.add("马云");
            dd.add("马化腾");
            dd.add("礼拜");
            FileUtils.writeLines(new File("happy.txt"), dd, "-", true);
    
            // 拷贝
            FileUtils.copyFile(new File("test.png"), new File("p-copy.png"));
            // 复制文件到目录
            FileUtils.copyFileToDirectory(new File("test.png"), new File("lib"));
            // 复制目录到目录下
            FileUtils.copyDirectoryToDirectory(new File("lib"), new File("lib2"));
            // 复制当前路径的某个目录到当前目录的新目录
            FileUtils.copyDirectory(new File("lib"), new File("lib2"));
            // 拷贝URL内容
            // 方法1:保存网上的图片到本地文件
            String url = "https://img-blog.csdnimg.cn/2019062009044675.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIxNTc5MDQ1,size_16,color_FFFFFF,t_70";
            FileUtils.copyURLToFile(new URL(url), new File("what.jpg"));
            // 方法2:获取网页的源码
            String dat = IOUtils.toString(new URL("http://www.baidu.com"), "UTF-8");
            System.out.println(dat);
        }
    }
    

第11章 多线程技术

1. 概念

  • Process与Thread

    img

  • 核心概念

    1. 线程就是独立的执行路径。
    2. 在程序运行时,即使没有自己创建线程,后台也会存在多个线程,如gc线程、主线程。
    3. main()称之为主线程,为系统的入口点,用于执行整个程序。
    4. 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为干预的。
    5. 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制。
    6. 线程会带来额外的开销,如cpu调度时间,并发控制开销。
    7. 每个线程在自己的工作内存交互,加载和存储主内存控制不当会造成数据不一致。
  • 少用继承多用实现,因为java里面只能单继承

  • 线程Thread的使用方式

    1. 继承Thread,重写run()方法,通过start()方法去启动线程
    2. 实现Runnable接口,重写run()方法,通过new一个Thead对象调start()方法。
  • start方法不保证立即运行,由cpu调用

    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: ThreadStudy01.java
     * @time: 2019/10/25 12:37
     * @desc: 进程学习1
     */
    
    public class StartThread1 extends Thread{
    
        public void run(){
            for (int i = 0; i < 20; i++) {
                System.out.println("一边听歌一边敲代码。");
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            // 创建子类对象
            StartThread1 st = new StartThread1();
            // 启动
            st.start();
            // run是普通方法的调用
    //        st.run();
            for (int i = 0; i < 20; i++) {
                System.out.println("coding。");
                Thread.sleep(1);
            }
        }
    }
    
  • 创建线程方式1:利用线程下载图片案例

    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: TDownloader.java
     * @time: 2019/10/28 15:58
     * @desc: 进程学习2:下载图片
     */
    
    public class TDownloader extends Thread{
        // 远程路径
        private String url;
        // 存储名字
        private String name;
    
        public TDownloader(String url, String name) {
            this.url = url;
            this.name = name;
        }
    
        @Override
        public void run() {
            WebDownloader wd = new WebDownloader();
            wd.download(url, name);
            System.out.println(name);
        }
        
        public static void main(String[] args){
            TDownloader td1 = new TDownloader("https://img-blog.csdnimg.cn/20181107085145510.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hhcHB5Um9ja2luZw==,size_16,color_FFFFFF,t_70", "lstm.png");
            TDownloader td2 = new TDownloader("https://img-blog.csdnimg.cn/20181107095455442.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hhcHB5Um9ja2luZw==,size_16,color_FFFFFF,t_70", "peephole_connection.png");
            TDownloader td3 = new TDownloader("https://img-blog.csdnimg.cn/20181107101049389.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hhcHB5Um9ja2luZw==,size_16,color_FFFFFF,t_70", "gru.png");
    
            // 启动三个线程
            td1.start();
            td2.start();
            td3.start();
        }
    }
    
  • 利用线程方式2:(推荐使用这种方式)

    • 避免单继承的局限性,优先使用接口
    • 方便共享资源
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: ThreadStudy01.java
     * @time: 2019/10/25 12:37
     * @desc: 进程学习3
     */
    
    public class StartRun1 implements Runnable {
    
        public void run() {
            for (int i = 0; i < 20; i++) {
                System.out.println("一边听歌一边敲代码。");
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            /*
            // 创建实现类对象
            StartRun1 sr = new StartRun1();
            // 创建代理类对象
            Thread t = new Thread(sr);
            // 启动
            t.start();
            // run是普通方法的调用
    //        st.run();
            */
    
            // 利用匿名对象
            new Thread(new StartRun1()).start();
    
            for (int i = 0; i < 20; i++) {
                System.out.println("coding。");
                Thread.sleep(1);
            }
        }
    }
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: TDownloader.java
     * @time: 2019/10/28 15:58
     * @desc: 进程学习2:下载图片
     */
    
    public class IDownloader implements Runnable {
        // 远程路径
        private String url;
        // 存储名字
        private String name;
    
        public IDownloader(String url, String name) {
            this.url = url;
            this.name = name;
        }
    
        @Override
        public void run() {
            WebDownloader wd = new WebDownloader();
            wd.download(url, name);
            System.out.println(name);
        }
    
        public static void main(String[] args) {
            IDownloader td1 = new IDownloader("https://img-blog.csdnimg.cn/20181107085145510.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hhcHB5Um9ja2luZw==,size_16,color_FFFFFF,t_70", "lstm.png");
            IDownloader td2 = new IDownloader("https://img-blog.csdnimg.cn/20181107095455442.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hhcHB5Um9ja2luZw==,size_16,color_FFFFFF,t_70", "peephole_connection.png");
            IDownloader td3 = new IDownloader("https://img-blog.csdnimg.cn/20181107101049389.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hhcHB5Um9ja2luZw==,size_16,color_FFFFFF,t_70", "gru.png");
    
            // 启动三个线程
            new Thread(td1).start();
            new Thread(td2).start();
            new Thread(td3).start();
        }
    }
    
  • 共享资源:模拟买票

    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: Web12306.java
     * @time: 2019/10/30 12:36
     * @desc: 共享资源:模拟买票
     */
    
    public class Web12306 implements Runnable {
        // 票数
        private int ticketNums = 99;
    
        @Override
        public void run() {
            while(true){
                if(ticketNums<0){
                    break;
                }
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "-->" + ticketNums--);
            }
        }
    
        public static void main(String[] args){
            // 一份资源
            Web12306 web = new Web12306();
            // 多个代理
            new Thread(web, "张三").start();
            new Thread(web, "李四").start();
            new Thread(web, "王五").start();
        }
    }
    
  • 共享资源:模拟龟兔赛跑

    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: Racer.java
     * @time: 2019/10/30 14:55
     * @desc: 共享资源:模拟龟兔赛跑
     */
    
    public class Racer implements Runnable {
        private String winner;       // 胜利者
    
        @Override
        public void run() {
            for (int steps = 1; steps <= 100; steps++) {
                // 模拟休息
                if(Thread.currentThread().getName().equals("rabit") && steps % 10 == 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "-->" + steps);
                // 比赛是否结束
                boolean flag = gameOver(steps);
                if (flag) {
                    break;
                }
            }
        }
    
        private boolean gameOver(int steps) {
            if (winner != null) {
                // 存在胜利者
                return true;
            } else {
                if (steps == 100) {
                    winner = Thread.currentThread().getName();
                    System.out.println("winner==>" + winner);
                    return true;
                }
            }
            return false;
        }
    
        public static void main(String[] args) {
            Racer racer = new Racer();
            new Thread(racer, "tortoise").start();
            new Thread(racer, "rabbit").start();
        }
    }
    
  • Callable:能抛出异常,有返回值(了解)

    import com.sun.org.apache.xpath.internal.operations.Bool;
    import jdk.nashorn.internal.codegen.CompilerConstants;
    
    import java.util.concurrent.*;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: TDownloader.java
     * @time: 2019/10/28 15:58
     * @desc: Callable了解学习
     */
    
    public class CDownloader implements Callable<Boolean> {
        // 远程路径
        private String url;
        // 存储名字
        private String name;
    
        public CDownloader(String url, String name) {
            this.url = url;
            this.name = name;
        }
    
        @Override
        public Boolean call() throws Exception {
            WebDownloader wd = new WebDownloader();
            wd.download(url, name);
            System.out.println(name);
            return true;
        }
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            CDownloader cd1 = new CDownloader("https://img-blog.csdnimg.cn/20181107085145510.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hhcHB5Um9ja2luZw==,size_16,color_FFFFFF,t_70", "lstm.png");
            CDownloader cd2 = new CDownloader("https://img-blog.csdnimg.cn/20181107095455442.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hhcHB5Um9ja2luZw==,size_16,color_FFFFFF,t_70", "peephole_connection.png");
            CDownloader cd3 = new CDownloader("https://img-blog.csdnimg.cn/20181107101049389.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hhcHB5Um9ja2luZw==,size_16,color_FFFFFF,t_70", "gru.png");
    
            // 创建执行服务
            ExecutorService ser = Executors.newFixedThreadPool(3);
            // 提交执行
            Future<Boolean> result1 = ser.submit(cd1);
            Future<Boolean> result2 = ser.submit(cd2);
            Future<Boolean> result3 = ser.submit(cd3);
            // 获取结果
            boolean r1 = result1.get();
            boolean r2 = result1.get();
            boolean r3 = result1.get();
            // 关闭服务
            ser.shutdownNow();
        }
    }
    
  • 创建线程有几种方式:常用的有两种,继承Thread类,重写Runnable接口。还有一种方式,JUC并发包下,实现Callable接口。

  • 静态代理设计模式

    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: StaticProxy.java
     * @time: 2019/10/30 15:29
     * @desc: 静态代理设计模式学习
     */
    
    public class StaticProxy {
        public static void main(String[] args) {
            new WeddingCompany(new You()).happyMarry();
        }
    }
    
    interface Marry {
        void happyMarry();
    }
    
    // 真实角色
    class You implements Marry {
        @Override
        public void happyMarry() {
            System.out.println("你和你的广寒仙子本月了...");
        }
    }
    
    //代理角色,婚庆公司
    class WeddingCompany implements Marry {
        // 真实角色
        private Marry target;
    
        public WeddingCompany(Marry target) {
            this.target = target;
        }
    
        @Override
        public void happyMarry() {
            ready();
            this.target.happyMarry();
            after();
        }
    
        private void ready() {
            System.out.println("布置猪窝...");
        }
    
        private void after() {
            System.out.println("闹玉兔...");
        }
    }
    
  • Lambda表达式 简化线程(用一次)的使用

    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: LambdaThread.java
     * @time: 2019/10/30 16:00
     * @desc: Lambda表达式 简化线程(用一次)的使用
     */
    
    public class LambdaThread {
        // 类中类:静态内部类
        static class Test implements Runnable {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("一边听歌");
                }
            }
        }
    
        public static void main(String[] args) {
            new Thread(new Test()).start();
    
            // 方法中类:局部内部类
            class Test2 implements Runnable {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        System.out.println("一边听歌");
                    }
                }
            }
            new Thread(new Test2()).start();
    
            // 参数中类:匿名内部类
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 20; i++) {
                        System.out.println("一边听歌");
                    }
                }
            }).start();
    
            // jdk8简化匿名内部类,lambda
            new Thread(
                    () -> {
                        for (int i = 0; i < 20; i++) {
                            System.out.println("一边听歌");
                        }
                    }
            ).start();
        }
    }
    
  • lambda推导:必须存在类型

    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: LambdaTest1.java
     * @time: 2019/10/31 15:18
     * @desc: lambda推导
     */
    
    public class LambdaTest1 {
        static class Like2 implements ILike {
            public void lambda() {
                System.out.println("2. 我喜欢你大爷!");
            }
        }
    
        public static void main(String[] args) {
            class Like3 implements ILike {
                public void lambda() {
                    System.out.println("3. 我喜欢你大爷!");
                }
            }
    
            // 外部类
            ILike like = new Like();
            like.lambda();
            // 静态内部类
            like = new Like2();
            like.lambda();
            // 方法内部类
            like = new Like3();
            like.lambda();
    
            // 匿名类
            like = new ILike() {
                @Override
                public void lambda() {
                    System.out.println("4. 我喜欢你大爷!");
                }
            };
            like.lambda();
    
            // lambda
            like = () -> {
                System.out.println("5. 我喜欢你大爷!");
            };
            like.lambda();
        }
    }
    
    interface ILike {
        void lambda();
    }
    
    class Like implements ILike {
        @Override
        public void lambda() {
            System.out.println("1. 我喜欢你大爷!");
        }
    }
    
  • lambda推导 + 参数

    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: LambdaTest1.java
     * @time: 2019/10/31 15:18
     * @desc: lambda推导 + 参数
     */
    
    public class LambdaTest2 {
        public static void main(String[] args) {
            ILove love = (int a) -> {
                System.out.println("偶买噶!-->" + a);
            };
            love.lambda(100);
    
            // 参数类型可以省略
            ILove love2 = s -> {
                System.out.println("偶买噶!-->" + s);
            };
            love2.lambda(10);
    
            // 花括号也可以省略
            ILove love3 = s -> System.out.println("偶买噶!-->" + s);
            love3.lambda(1);
        }
    }
    
    interface ILove {
        void lambda(int a);
    }
    
    class Love implements ILove {
        @Override
        public void lambda(int a) {
            System.out.println("偶买噶!-->" + a);
        }
    }
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: LambdaTest1.java
     * @time: 2019/10/31 15:18
     * @desc: lambda推导 + 参数
     */
    
    public class LambdaTest2 {
        public static void main(String[] args) {
            ILove love = (int a) -> {
                System.out.println("偶买噶!-->" + a);
            };
            love.lambda(100);
    
            // 参数类型可以省略
            ILove love2 = s -> {
                System.out.println("偶买噶!-->" + s);
            };
            love2.lambda(10);
    
            // 花括号也可以省略
            ILove love3 = s -> System.out.println("偶买噶!-->" + s);
            love3.lambda(1);
        }
    }
    
    interface ILove {
        void lambda(int a);
    }
    
    class Love implements ILove {
        @Override
        public void lambda(int a) {
            System.out.println("偶买噶!-->" + a);
        }
    }
    
  • lambda推导 + 参数 + 返回值

    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: LambdaTest1.java
     * @time: 2019/10/31 15:18
     * @desc: lambda推导 + 参数 + 返回值
     */
    
    public class LambdaTest3 {
        public static void main(String[] args) {
            IInterest in = (int q, int p) -> {
                System.out.println(q + p);
                return q + p;
            };
            in.lambda(100, 50);
    
            // 简化版本
            IInterest in2 = (q, p) -> q + p / 2;
            System.out.println(in2.lambda(10, 20));
        }
    }
    
    interface IInterest {
        int lambda(int a, int b);
    }
    
    // 参考,下面内容可以不要
    class Interest implements IInterest {
        @Override
        public int lambda(int aa, int bb) {
            System.out.println(aa + bb);
            return aa + bb;
        }
    }
    
  • lambda推导实现线程

    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: LambdaTest1.java
     * @time: 2019/10/31 15:18
     * @desc: lambda推导实现线程
     */
    
    public class LambdaTest4 {
        public static void main(String[] args) {
            new Thread(() -> {
                System.out.println("一边学习lambda");
            }).start();
    
            // 简化:花括号可以不要
            new Thread(() -> System.out.println("一边泪流满面")).start();
    
            // 如果是多个语句,就不能省略
            new Thread(() -> {
                for (int i = 0; i < 20; i++) {
                    System.out.println("我疯了,你呢?");
                }
            }).start();
        }
    }
    

2. 线程状态

  • 一个线程对象在它的生命周期内,需要经历5个状态。

    图11-4 线程生命周期图.png

  1. 新生状态(New)

    用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。

  2. 就绪状态(Runnable)

    处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。有4中原因会导致线程进入就绪状态:

    1. 新建线程:调用start()方法,进入就绪状态;

    2. 阻塞线程:阻塞解除,进入就绪状态;

    3. 运行线程:调用yield()方法,直接进入就绪状态;

    4. 运行线程:JVM将CPU资源从本线程切换到其他线程。

  3. 运行状态(Running)

    在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。

  4. 阻塞状态(Blocked)

    阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。有4种原因会导致阻塞:

    1. 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。

    2. 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。

    3. 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。

    4. join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。

  5. 死亡状态(Terminated)

    死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它run()方法内的全部工作; 另一个是线程被强制终止,如通过执行stop()或destroy()方法来终止一个线程(注:stop()/destroy()方法已经被JDK废弃,不推荐使用)。

    当一个线程进入死亡状态以后,就不能再回到其它状态了。

  • 线程的终止

    1. 线程正常执行完毕-->次数
    2. 外部干涉–>加入标识

    不要使用stop和destroy

    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: TerminateThread.java
     * @time: 2019/11/1 14:32
     * @desc: 终止线程
     */
    
    public class TerminateThread implements Runnable {
        // 1. 设置标识,标记线程体是否可以运行
        private boolean flag = true;
        private String name;
    
        public TerminateThread(String name) {
            this.name = name;
        }
    
        @Override
        public void run() {
            int i = 0;
            // 2. 关联标识,true-->运行,False-->停止
            while (flag) {
                System.out.println(name + "-->" + i++);
            }
        }
    
        // 3. 对外提供方法改变标识
        public void terminate() {
            this.flag = false;
        }
    
        public static void main(String[] args) {
            TerminateThread tt = new TerminateThread("你大爷");
            new Thread(tt).start();
            for (int i = 0; i < 99; i++) {
                if (i == 88){
                    tt.terminate();     // 线程终止
                    System.out.println("tt game over!");
                }
                System.out.println("main-->" + i);
            }
        }
    }
    
  • 线程的暂停-sleep: 可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。

    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: BlockedSleep1.java
     * @time: 2019/11/1 14:46
     * @desc: sleep模拟倒计时
     */
    
    public class BlockedSleep1 {
        public static void main(String[] args) throws InterruptedException {
            // 倒计时
            Date endTime = new Date(System.currentTimeMillis() + 1000 * 10);
            long end = endTime.getTime();
            while (true) {
                System.out.println(new SimpleDateFormat("mm:ss").format(endTime));
                Thread.sleep(1000);
                endTime = new Date(endTime.getTime()-1000);
                if(end-10000 > endTime.getTime()){
                    break;
                }
            }
        }
    
        public static void test() throws InterruptedException {
            // 倒数10个数,1秒一个
            int num = 10;
            while (true) {
                Thread.sleep(1000);
                System.out.println(num--);
            }
        }
    }
    
  • 线程的暂停-yield: 可以让正在运行的线程直接进入就绪状态,让出CPU的使用权。

    import org.omg.PortableServer.THREAD_POLICY_ID;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: YieldDemo1.java
     * @time: 2019/11/1 14:55
     * @desc: yield礼让线程,暂停线程,直接进入就绪状态不是阻塞状态
     */
    
    public class YieldDemo1 {
        public static void main(String[] args) {
            MyYield my = new MyYield();
            new Thread(my, "a").start();
            new Thread(my, "b").start();
    
            // lambda实现
            new Thread(() -> {
                for (int i = 0; i < 100; i++) {
                    System.out.println("lambda..." + i);
                }
            }).start();
            for (int i = 0; i < 100; i++) {
                if (i % 20 == 0) {
                    Thread.yield();     // main礼让
                }
                System.out.println("main..." + i);
            }
        }
    }
    
    class MyYield implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "-->start");
            Thread.yield();     // 礼让
            System.out.println(Thread.currentThread().getName() + "-->end");
        }
    }
    
  • 线程的联合-join:合并线程,插队线程。

    import sun.java2d.loops.TransformHelper;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: BlockedJoin1.java
     * @time: 2019/11/1 15:05
     * @desc: 爸爸和儿子买烟的故事
     */
    
    public class BlockedJoin1 {
        public static void main(String[] args){
            new Father().start();
        }
    }
    
    class Father extends Thread{
        @Override
        public void run() {
            System.out.println("想抽烟,发现没了");
            System.out.println("让儿子去买中华");
            Thread t = new Son();
            t.start();
            try {
                t.join();       // father被阻塞
                System.out.println("老爸接过烟,把零钱给了儿子");
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("孩子走丢了,老爸出去找孩子去了...");
            }
        }
    }
    
    class Son extends Thread{
        @Override
        public void run() {
            System.out.println("接过老爸的钱出去了...");
            System.out.println("路边有个游戏厅,玩了10秒");
            for (int i = 0; i < 10; i++) {
                System.out.println(i+"秒过去了...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("赶紧买烟去...");
            System.out.println("手拿一包中华回家了...");
        }
    }
    
  • 观察线程的各个状态

    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: AllState.java
     * @time: 2019/11/1 15:22
     * @desc: 观察线程的各个状态
     */
    
    public class AllState {
        public static void main(String[] args) {
            Thread t = new Thread(() -> {
                for (int i = 0; i < 5; i++) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("...");
            });
            // 观察状态
            Thread.State state = t.getState();
            System.out.println(state);  // NEW
            t.start();
            state = t.getState();
            System.out.println(state);  // RUNNABLE
    
            while (state != Thread.State.TERMINATED) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                state = t.getState();   // TIMED_WAITING
                System.out.println(state);
            }
            state = t.getState();   // TERMINATED
            System.out.println(state);
        }
    }
    

3. 线程的优先级

  1. NORM_PRIORITY 5

  2. MIN_PRIORITY 1

  3. MAX_PRIORITY 10

    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: PriorityTest1.java
     * @time: 2019/11/4 12:38
     * @desc: 多线程优先级
     */
    
    public class PriorityTest1 {
        public static void main(String[] args) {
            MyPriority mp = new MyPriority();
            Thread t1 = new Thread(mp);
            Thread t2 = new Thread(mp);
            Thread t3 = new Thread(mp);
            Thread t4 = new Thread(mp);
            Thread t5 = new Thread(mp);
            Thread t6 = new Thread(mp);
    
            t1.setPriority(Thread.MAX_PRIORITY);
            t2.setPriority(Thread.MAX_PRIORITY);
            t3.setPriority(Thread.MAX_PRIORITY);
            t4.setPriority(Thread.MIN_PRIORITY);
            t5.setPriority(Thread.MIN_PRIORITY);
            t6.setPriority(Thread.MIN_PRIORITY);
    
            t1.start();
            t2.start();
            t3.start();
            t4.start();
            t5.start();
            t6.start();
        }
    }
    
    class MyPriority implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
            Thread.yield();
        }
    }
    

4. 守护线程

  • 是为用户线程服务的;JVM停止不用等待守护线程执行完毕

  • 默认:用户线程,JVM等待用户线程执行完毕才会停止

    import org.omg.PortableServer.THREAD_POLICY_ID;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: DaemonTest.java
     * @time: 2019/11/4 13:35
     * @desc: 守护线程学习
     */
    
    public class DaemonTest {
        public static void main(String[] args) {
            Thread t1 = new Thread(new You1());
            t1.run();
            Thread t2 = new Thread(new God1());
            // 将用户线程调整为守护线程
            t2.setDaemon(true);
            t2.start();
        }
    }
    
    class You1 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 365 * 100; i++) {
                System.out.println("happy life!");
            }
            System.out.println("ooo...");
        }
    }
    
    class God1 extends Thread {
        @Override
        public void run() {
            for (;true;) {
                System.out.println("bless you!");
            }
        }
    }
    

5. 获取线程基本信息的方法

  • 常用方法

    表11-1线程的常用方法.png

  • 案例

    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: InfoTest.java
     * @time: 2019/11/4 13:46
     * @desc: 获取线程基本信息的方法
     */
    
    public class InfoTest {
        public static void main(String[] args) throws InterruptedException {
            // 线程是否活着
            System.out.println(Thread.currentThread().isAlive());
            // 设置名称:真是角色+代理角色
            MyInfo info = new MyInfo("战斗机");
            Thread t = new Thread(info);
            t.setName("公鸡");
            t.start();
            Thread.sleep(1000);
            System.out.println(t.isAlive());
        }
    }
    
    class MyInfo implements Runnable{
        private String name;
        public MyInfo(String name) {
            this.name = name;
        }
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "-->" + name);
        }
    }
    

6. 并发控制

  • 并发:同一个对象多个线程同时操作

1. 同步

  • 线程不安全案例1

    package com.sxt.thread;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: UnsafeTest.java
     * @time: 2019/11/4 13:57
     * @desc: 线程同步
     */
    
    public class UnsafeTest {
        public static void main(String[] args) {
            // 账户
            Account account = new Account(100, "结婚礼金");
            Drawing you = new Drawing(account, 80, "可悲的你");
            Drawing wife = new Drawing(account, 90, "happy的她");
            you.start();
            wife.start();
        }
    }
    
    // 账户
    class Account {
        int money;
        String name;
    
        public Account(int money, String name) {
            this.money = money;
            this.name = name;
        }
    }
    
    // 模拟取款
    class Drawing extends Thread {
        // 取钱的账户
        Account accout;
        // 取多少钱
        int drawingMoney;
        // 口袋里的总数
        int packetTotal;
    
        public Drawing(Account accout, int drawingMoney, String name) {
            super(name);
            this.accout = accout;
            this.drawingMoney = drawingMoney;
        }
    
        @Override
        public void run() {
            if(accout.money - drawingMoney < 0){
                return;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            accout.money -= drawingMoney;
            packetTotal += drawingMoney;
            System.out.println(this.getName() + "-->账户余额为:" + accout.money);
            System.out.println(this.getName() + "-->口袋里的钱为:" + packetTotal);
        }
    }
    
  • 线程不安全案例2

    package com.sxt.thread;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: UnsafeTest.java
     * @time: 2019/11/4 13:57
     * @desc: 线程同步
     */
    
    public class UnsafeTest2 {
        public static void main(String[] args) {
            List<String> list = new ArrayList<>();
            for (int i = 0; i < 10000; i++) {
                new Thread(()->{
                    list.add(Thread.currentThread().getName());
                }).start();
            }
            System.out.println(list.size());
        }
    }
    
  • 锁机制

    • 为了保证数据在方法中被访问时的正确性,在访问时加入锁机制(synchronized),当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题:

      1. 一个线程持有锁会导致其它所有需要此锁的线程挂起;
      2. 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
      3. 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。
    • 线程安全:在并发时保证数据的正确性、效率尽可能高(synchronized)

      • 同步方法
      • 同步块(java有四种块,普通块局部块,构造块,静态块,同步块)
    • 样例1:

      package com.sxt.thread;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: UnsafeTest.java
       * @time: 2019/11/4 13:57
       * @desc: 线程同步
       */
      
      public class SafeTest {
          public static void main(String[] args) {
              // 账户
              Account account = new Account(100, "结婚礼金");
              SafeDrawing you = new SafeDrawing(account, 80, "可悲的你");
              SafeDrawing wife = new SafeDrawing(account, 90, "happy的她");
              you.start();
              wife.start();
          }
      }
      
      // 模拟取款
      class SafeDrawing extends Thread {
          // 取钱的账户
          Account accout;
          // 取多少钱
          int drawingMoney;
          // 口袋里的总数
          int packetTotal;
      
          public SafeDrawing(Account accout, int drawingMoney, String name) {
              super(name);
              this.accout = accout;
              this.drawingMoney = drawingMoney;
          }
      
          @Override
          public void run() {
              test();
          }
      
          public void test() {
              if (accout.money <= 0) {
                  return;
              }
              synchronized (accout) {
                  if (accout.money - drawingMoney < 0) {
                      return;
                  }
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  accout.money -= drawingMoney;
                  packetTotal += drawingMoney;
                  System.out.println(this.getName() + "-->账户余额为:" + accout.money);
                  System.out.println(this.getName() + "-->口袋里的钱为:" + packetTotal);
              }
          }
      }
      
    • 样例2

      package com.sxt.thread;
      
      import java.util.ArrayList;
      import java.util.List;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: UnsafeTest.java
       * @time: 2019/11/4 13:57
       * @desc: 线程同步
       */
      
      public class SafeTest2 {
          public static void main(String[] args) {
              List<String> list = new ArrayList<>();
              for (int i = 0; i < 10000; i++) {
                  new Thread(() -> {
                      // 同步块
                      synchronized (list) {
                          list.add(Thread.currentThread().getName());
                      }
                  }).start();
              }
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(list.size());
          }
      }
      
  • 双重检测:考虑临界值的问题

package com.sxt.thread;

/**
 * @author: Li Tian
 * @contact: litian_cup@163.com
 * @software: IntelliJ IDEA
 * @file: Web12306.java
 * @time: 2019/10/30 12:36
 * @desc: 线程安全买票
 */

public class Safe12306 implements Runnable {
    // 票数
    private int ticketNums = 10;
    private boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            test();
        }
    }

    private void test() {
        if (ticketNums <= 0) {  // 考虑的是没有票的情况
            flag = false;
            return;
        }
        synchronized (this) {
            if (ticketNums <= 0) {  // 考虑的是最后一张票的情况
                flag = false;
                return;
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-->" + ticketNums--);
        }
    }

    public static void main(String[] args) {
        // 一份资源
        Safe12306 web = new Safe12306();
        // 多个代理
        new Thread(web, "张三").start();
        new Thread(web, "李四").start();
        new Thread(web, "王五").start();
    }
}
  • 案例1:快乐影院

    package com.sxt.thread;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: HappyCinema.java
     * @time: 2019/11/5 12:57
     * @desc: 快乐电影院抢座位案例
     */
    
    public class HappyCinema {
        public static void main(String[] args) {
            Cinema c = new Cinema(2, "happy sxt");
            new Thread(new Customer(c, 2), "老高").start();
            new Thread(new Customer(c, 1), "老李").start();
        }
    }
    
    class Customer implements Runnable {
        Cinema cinema;
        int seats;
    
        public Customer(Cinema cinema, int seats) {
            this.cinema = cinema;
            this.seats = seats;
        }
    
        @Override
        public void run() {
            synchronized (cinema) {
                boolean flag = cinema.bookTickets(seats);
                if (flag) {
                    System.out.println("出票成功" + Thread.currentThread().getName() + "-<位置为:" + seats);
                } else {
                    System.out.println("出票失败" + Thread.currentThread().getName() + "-<位置不够!");
                }
            }
        }
    }
    
    class Cinema {
        // 可用的位置
        int available;
        // 名称
        String name;
    
        public Cinema(int available, String name) {
            this.available = available;
            this.name = name;
        }
    
        // 购票
        public boolean bookTickets(int seats) {
            System.out.println("可用位置为:" + available);
            if (seats > available) {
                return false;
            }
            available -= seats;
            return true;
        }
    }
    
  • 案例2:快乐影院真实List座位

    package com.sxt.thread;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: HappyCinema.java
     * @time: 2019/11/5 12:57
     * @desc: 快乐电影院抢座位案例
     */
    
    public class HappyCinema2 {
        public static void main(String[] args) {
            // 可用位置
            List<Integer> available = new ArrayList<>();
            for (int i = 1; i < 8; i++) {
                available.add(i);
            }
    
            // 顾客需要的位置
            List<Integer> seats1 = new ArrayList<>();
            seats1.add(1);
            seats1.add(2);
            List<Integer> seats2 = new ArrayList<>();
            seats2.add(4);
            seats2.add(5);
            seats2.add(6);
    
            SxtCinema c = new SxtCinema(available, "happy sxt");
            new Thread(new HappyCustomer(c, seats1), "老高").start();
            new Thread(new HappyCustomer(c, seats2), "老李").start();
        }
    }
    
    class HappyCustomer implements Runnable {
        SxtCinema cinema;
        List<Integer> seats;
    
        public HappyCustomer(SxtCinema cinema, List<Integer> seats) {
            this.cinema = cinema;
            this.seats = seats;
        }
    
        @Override
        public void run() {
            synchronized (cinema) {
                boolean flag = cinema.bookTickets(seats);
                if (flag) {
                    System.out.println("出票成功" + Thread.currentThread().getName() + "-<位置为:" + seats);
                } else {
                    System.out.println("出票失败" + Thread.currentThread().getName() + "-<位置不够!");
                }
            }
        }
    }
    
    class SxtCinema {
        // 可用的位置
        List<Integer> available;
        // 名称
        String name;
    
        public SxtCinema(List<Integer> available, String name) {
            this.available = available;
            this.name = name;
        }
    
        // 购票
        public boolean bookTickets(List<Integer> seats) {
            System.out.println("可用位置为:" + available);
            List<Integer> copy = new ArrayList<>();
            copy.addAll(available);
    
            // 相减
            copy.removeAll(seats);
            // 判断大小
            if (available.size() - copy.size() != seats.size()) {
                return false;
            }
            // 成功
            available = copy;
    
            return true;
        }
    }
    
  • 案例3:快乐火车票

    package com.sxt.thread;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: Happy12306.java
     * @time: 2019/11/7 19:24
     * @desc: 快乐火车票
     */
    
    public class Happy12306 {
        public static void main(String[] args) {
            Web12306 c = new Web12306(2, "happy sxt");
            new Passenger(c, "老高", 2).start();
            new Passenger(c, "老李", 1).start();
        }
    }
    
    // 乘客
    class Passenger extends Thread {
        int seats;
    
        public Passenger(Runnable target, String name, int seats) {
            super(target, name);
            this.seats = seats;
        }
    }
    
    // 火车票网
    class Web12306 implements Runnable {
        // 可用的位置
        int available;
        // 名称
        String name;
    
        public Web12306(int available, String name) {
            this.available = available;
            this.name = name;
        }
    
        @Override
        public void run() {
            Passenger p = (Passenger) Thread.currentThread();
            boolean flag = this.bookTickets(p.seats);
            if (flag) {
                System.out.println("出票成功" + Thread.currentThread().getName() + "-<位置为:" + p.seats);
            } else {
                System.out.println("出票失败" + Thread.currentThread().getName() + "-<位置不够!");
            }
        }
    
        // 购票
        public synchronized boolean bookTickets(int seats) {
            System.out.println("可用位置为:" + available);
            if (seats > available) {
                return false;
            }
            available -= seats;
            return true;
        }
    }
    
  • 并发容器:import java.util.concurrent.CopyOnWriteArrayList

    package com.sxt.thread;
    
    import java.util.concurrent.CopyOnWriteArrayList;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: SynContainer.java
     * @time: 2019/11/8 14:09
     * @desc: 线程同步:并发容器
     */
    
    public class SynContainer {
        public static void main(String[] args) {
            CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
            for (int i = 0; i < 10000; i++) {
                new Thread(() -> {
                    // 同步块
                    list.add(Thread.currentThread().getName());
                }).start();
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(list.size());
        }
    }
    

2. 死锁

  • 死锁指的是:多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。

  • 避免方式:不要在同一个代码块中持有多个对象锁。

  • 死锁案例:

    package com.sxt.thread;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: DeadLock.java
     * @time: 2019/11/8 14:16
     * @desc: 死锁
     */
    
    public class DeadLock {
        public static void main(String[] args) {
            Makeup g1 = new Makeup(1, "丰光");
            Makeup g2 = new Makeup(2, "师兄");
            g1.start();
            g2.start();
        }
    }
    
    // 口红
    class Lipstick {
    
    }
    
    // 镜子
    class Mirror {
    
    }
    
    // 化妆
    class Makeup extends Thread {
        static Lipstick lip = new Lipstick();
        static Mirror mir = new Mirror();
        // 选择
        int choice;
        // 名字
        String girlname;
    
        public Makeup(int choice, String girlname) {
            this.choice = choice;
            this.girlname = girlname;
        }
    
        @Override
        public void run() {
            // 化妆
            makeup();
        }
    
        private void makeup() {
            // 相互持有对方的对象锁,这样才有可能造成死锁
            if (choice == 1) {
                // 获得口红的锁
                synchronized (lip) {
                    System.out.println(this.girlname + "-->涂口红");
                    // 1秒后想拥有镜子的锁
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (mir) {
                        System.out.println(this.girlname + "-->照镜子");
                    }
                }
            } else {
                synchronized (mir) {
                    System.out.println(this.girlname + "-->照镜子");
                    // 2秒后想拥有口红的锁
                    try {
                        Thread.sleep(1100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lip) {
                        System.out.println(this.girlname + "-->涂口红");
                    }
                }
            }
        }
    }
    
  • 死锁的解决案例:

    package com.sxt.thread;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: DeadLock.java
     * @time: 2019/11/8 14:16
     * @desc: 解决死锁
     */
    
    public class DeadLock2 {
        public static void main(String[] args) {
            Makeup2 g1 = new Makeup2(1, "丰光");
            Makeup2 g2 = new Makeup2(2, "师兄");
            g1.start();
            g2.start();
        }
    }
    
    
    // 化妆
    class Makeup2 extends Thread {
        static Lipstick lip = new Lipstick();
        static Mirror mir = new Mirror();
        // 选择
        int choice;
        // 名字
        String girlname;
    
        public Makeup2(int choice, String girlname) {
            this.choice = choice;
            this.girlname = girlname;
        }
    
        @Override
        public void run() {
            // 化妆
            makeup();
        }
    
        private void makeup() {
            // 相互持有对方的对象锁,这样才有可能造成死锁
            if (choice == 1) {
                // 获得口红的锁
                synchronized (lip) {
                    System.out.println(this.girlname + "-->涂口红");
                    // 1秒后想拥有镜子的锁
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                synchronized (mir) {
                    System.out.println(this.girlname + "-->照镜子");
                }
            } else {
                synchronized (mir) {
                    System.out.println(this.girlname + "-->照镜子");
                    // 2秒后想拥有口红的锁
                    try {
                        Thread.sleep(1100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                synchronized (lip) {
                    System.out.println(this.girlname + "-->涂口红");
                }
            }
        }
    }
    

3. 并发协作

  • 生产者消费者模式

  • view简介

    • pv:page view
    • uv:unique view
    • vv:visit view
  • 在生产者消费者问题中,仅有synchronized是不够的

    • synchronized可组织并发更新同一个共享资源,实现了同步
    • synchronized不能用来实现不同线程之间的消息传递(通信)
  • 实现生产者消费者的方法:

    • 管程法
    • 信号灯法
  • 实现方式:用wait()等待,notify()唤醒

  • 管程法:借助缓冲区

    package com.sxt.cooperation;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: CoTest1.java
     * @time: 2019/11/8 15:36
     * @desc: 协作模型:生产者消费者实现方式1:管程法
     */
    
    public class CoTest1 {
        public static void main(String[] args) {
            SynContainer container = new SynContainer();
            new Productor(container).start();
            new Consumer(container).start();
        }
    }
    
    // 生产者
    class Productor extends Thread {
        SynContainer container;
    
        public Productor(SynContainer container) {
            this.container = container;
        }
    
        @Override
        public void run() {
            // 开始生产
            for (int i = 0; i < 100; i++) {
                System.out.println("生产-->第" + i + "个馒头");
                container.push(new SteamedBun(i));
            }
        }
    }
    
    // 消费者
    class Consumer extends Thread {
        SynContainer container;
    
        public Consumer(SynContainer container) {
            this.container = container;
        }
    
        @Override
        public void run() {
            // 开始消费
            for (int i = 0; i < 1000; i++) {
                System.out.println("消费-->第" + container.pop().id + "个馒头");
            }
        }
    }
    
    // 缓冲区
    class SynContainer {
        SteamedBun[] buns = new SteamedBun[10];
        int count = 0;
    
        // 存储:生产
        public synchronized void push(SteamedBun bun) {
            // 何时能生产:容器存在空间
            if (count == buns.length) {
                try {
                    // 线程阻塞,消费者通知生产解除
                    this.wait();
                } catch (InterruptedException e) {
                }
            }
            // 存在空间,可以生产
            buns[count++] = bun;
            // 存在数据了,可以通知消费了
            this.notifyAll();
        }
    
        // 获取:消费
        public synchronized SteamedBun pop() {
            // 何时消费:容器中是否存在数据,存在数据则可以消费,没有数据就只能等待
            if (count == 0) {
                try {
                    // 线程阻塞:生产者通知消费则接触阻塞
                    this.wait();
                } catch (InterruptedException e) {
                }
            }
            SteamedBun bun = buns[--count];
            // 存在空间,可以唤醒对方生产
            this.notifyAll();
            return bun;
        }
    }
    
    // 数据。馒头
    class SteamedBun {
        int id;
    
        public SteamedBun(int id) {
            this.id = id;
        }
    }
    
  • 信号灯法:借助标志位

    package com.sxt.cooperation;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: CoTest2.java
     * @time: 2019/11/8 16:38
     * @desc: 信号灯法
     */
    
    public class CoTest2 {
        public static void main(String[] args) {
            Tv tv = new Tv();
            new Player(tv).start();
            new Watcher(tv).start();
        }
    }
    
    // 生产者:演员
    class Player extends Thread {
        Tv tv;
    
        public Player(Tv tv) {
            this.tv = tv;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                if (i % 2 == 0) {
                    this.tv.play("奇葩说");
                } else {
                    this.tv.play("倚天屠龙记");
                }
            }
        }
    }
    
    // 消费者:观众
    class Watcher extends Thread {
        Tv tv;
    
        public Watcher(Tv tv) {
            this.tv = tv;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                tv.watch();
            }
        }
    }
    
    // 同一个资源:电视
    class Tv {
        String voice;
        // T:演员表演,观众等待;F:观众观看,演员等待
        boolean flag = true;
    
        // 表演
        public synchronized void play(String voice) {
            // 演员等待
            if (!flag) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("表演了" + voice);
            this.voice = voice;
            // 唤醒
            this.notifyAll();
            this.flag = !this.flag;
        }
    
        // 观看
        public synchronized void watch() {
            // 观众等待
            if (flag) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("听到了" + voice);
            // 唤醒
            this.notifyAll();
            this.flag = !this.flag;
        }
    }
    

7. 高级主题

  • 定时调度(简单):Timer和TimerTask类

    package com.sxt.cooperation;
    
    import com.sun.deploy.cache.CacheEntry;
    import com.sun.deploy.security.MozillaMyKeyStore;
    
    import java.util.Calendar;
    import java.util.GregorianCalendar;
    import java.util.Timer;
    import java.util.TimerTask;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: TimerTest1.java
     * @time: 2019/11/9 18:27
     * @desc: 定时调度
     */
    
    public class TimerTest1 {
        public static void main(String[] args) {
            Timer timer = new Timer();
            // 执行安排
            // 执行一次
            timer.schedule(new MyTask(), 1000);
            // 执行多次
            timer.schedule(new MyTask(), 1000, 200);
            // 指定时间执行
            Calendar cal = new GregorianCalendar(2099, 11, 3, 11, 22, 22);
            timer.schedule(new MyTask(), cal.getTime(), 200);
        }
    }
    
    class MyTask extends TimerTask {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("放空大脑休息一会儿~");
            }
        }
    }
    
  • 定时调度(复杂):QUARTZ

    package com.sxt.others;
    
    import static org.quartz.DateBuilder.evenSecondDate;
    import static org.quartz.JobBuilder.newJob;
    import static org.quartz.TriggerBuilder.newTrigger;
    import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
    
    import org.quartz.JobDetail;
    import org.quartz.Scheduler;
    import org.quartz.SchedulerFactory;
    import org.quartz.Trigger;
    import org.quartz.impl.StdSchedulerFactory;
    
    import java.util.Date;
    
    /**
     * quartz学习入门
     */
    public class QuartzTest {
        public void run() throws Exception {
            // 1. 创建Scheduler的工厂
            SchedulerFactory sf = new StdSchedulerFactory();
            // 2. 从工厂中获取调度器
            Scheduler sched = sf.getScheduler();
            // 时间
            Date runTime = evenSecondDate(new Date());
            // 3. 创建JobDetail
            JobDetail job = newJob(HelloJob.class).withIdentity("job1", "group1").build();
    
            // 4. 触发器
            // Trigger trigger = newTrigger().withIdentity("trigger1", "group1").startAt(runTime).build();
            // 4 | 2:如果想要循环多次呢,每5秒一次,循环三次
            Trigger trigger = newTrigger().withIdentity("trigger1", "group1").startAt(runTime)
                    .withSchedule(simpleSchedule().withIntervalInSeconds(5).withRepeatCount(2)).build();
            // 5. 注册任务和触发条件
            sched.scheduleJob(job, trigger);
            // 6. 启动
            sched.start();
            try {
                // 5秒后停止
                Thread.sleep(30L * 1000L);
                // executing...
            } catch (Exception e) {
            }
    
            // shut down the scheduler
            sched.shutdown(true);
        }
    
        public static void main(String[] args) throws Exception {
            QuartzTest example = new QuartzTest();
            example.run();
        }
    }
    
  • 指令重排HappenBefore

    • 执行代码的顺序可能与编写代码不一致,即虚拟机优化代码顺序,则为指令重排(HappenBefore)——优化程序性能。
    • 机器语言运行步骤
      1. 从内存中获取要执行的下一个指令
      2. 将指令解码翻译,从寄存器中取值
      3. 操作,计算结果
      4. 将结果写回到对应的寄存器中
  • volatile:

    • 保证线程间变量的可见性,即保证数据的同步。

    • volatile是不错的机制,但是volatile不能保证原子性。

      package com.sxt.others;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: VolatileTest.java
       * @time: 2019/11/11 9:29
       * @desc: volatile测试
       * 不加volatile则程序不会停,加了之后会停
       */
      
      public class VolatileTest {
          private volatile static int num = 0;
          public static void main(String[] args) throws InterruptedException {
              new Thread(() -> {
                  while(num == 0){
                      // 此处不要编写代码,这是为了让系统没有时间更新数据
                  }
              }).start();
      
              Thread.sleep(1000);
              num = 1;
          }
      }
      
  • dcl单例模式

    • 懒汉式套路的基础上加入并发控制,保证在多线程环境下,对外存在一个对象
    1. 构造器私有化 --> 避免外部new构造器

    2. 提供私有的静态属性 --> 存储对象的地址

    3. 提供公共的静态方法 --> 获取属性

      package com.sxt.others;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: DoubleCheckedLocking.java
       * @time: 2019/11/11 9:34
       * @desc: 单例模式
       */
      
      public class DoubleCheckedLocking {
          // 2. 提供私有的静态属性
          // 没有volatile其他线程可能访问一个没有初始化的对象
          private static volatile DoubleCheckedLocking instance;
      
          // 1. 构造器私有化
          private DoubleCheckedLocking() {
      
          }
      
          // 3. 提供公共的静态方法 --> 获取属性
          public static DoubleCheckedLocking getInstance() {
              // 再次检测,避免不必要的同步,已经存在对象
              if (null != instance) {
                  return instance;
              }
              synchronized (DoubleCheckedLocking.class) {
                  if (null == instance) {
                      instance = new DoubleCheckedLocking();
                      // new一个对象的时候,要做的三件事情
                      // 开辟空间;初始化对象信息;返回对象的地址给引用
                      // 所以这里可能出现指令重排
                  }
                  return instance;
              }
          }
      
          public static void main(String[] args) {
              Thread t = new Thread(() -> {
                  System.out.println(DoubleCheckedLocking.getInstance());
              });
              t.start();
              System.out.println(DoubleCheckedLocking.getInstance());
          }
      }
      
  • ThreadLocal

    • 表示的是每个线程自身的存储本地、局部区域

    • 方法:get/set/initialValue

      package com.sxt.others;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: ThreadLocalTest.java
       * @time: 2019/11/11 9:52
       * @desc: ThreadLocal
       */
      
      public class ThreadLocalTest {
          //    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
          // 更改初始值
      //    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
      //        @Override
      //        protected Integer initialValue() {
      //            return 200;
      //        }
      //    };
          // 简化上面代码
          private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 200);
      
      
          public static void main(String[] args) {
              // 获取值,初始值为null
              System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get());
              // 设置值
              threadLocal.set(99);
              System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get());
      
              new Thread(new MyRun()).start();
              new Thread(new MyRun()).start();
          }
      
          public static class MyRun implements Runnable {
              @Override
              public void run() {
                  threadLocal.set((int)(Math.random()*99));
                  System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get());
              }
          }
      }
      
    • 每个线程只使用自身的数据,更改不会影响其他线程

      package com.sxt.others;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: ThreadLocalTest2.java
       * @time: 2019/11/11 10:06
       * @desc: 取数据
       */
      
      public class ThreadLocalTest2 {
          private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
      
          public static void main(String[] args) {
              for (int i = 0; i < 5; i++) {
                  new Thread(new MyRun()).start();
              }
          }
      
          public static class MyRun implements Runnable {
              @Override
              public void run() {
                  Integer left = threadLocal.get();
                  System.out.println(Thread.currentThread().getName() + "得到了-->" + left);
                  threadLocal.set(left - 1);
                  System.out.println(Thread.currentThread().getName() + "还剩下-->" + threadLocal.get());
              }
          }
      }
      
    • ThreadLocal:分析上下文环境

      • 构造器:哪里调用,就属于哪里,找线程体

      • run方法:本线程自己的

        package com.sxt.others;
        
        /**
         * @author: Li Tian
         * @contact: litian_cup@163.com
         * @software: IntelliJ IDEA
         * @file: ThreadLocalTest3.java
         * @time: 2019/11/11 10:11
         * @desc: 分析上下文环境
         */
        
        public class ThreadLocalTest3 {
            private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
        
            public static void main(String[] args) {
                new Thread(new MyRun()).start();
                new Thread(new MyRun()).start();
            }
        
            public static class MyRun implements Runnable {
                public MyRun() {
                    // 属于main线程
                    threadLocal.set(-100);
                    System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get());
                }
        
                @Override
                public void run() {
                    // 属于其他线程
                    System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get());
                }
            }
        }
        
    • InheritableThreadLocal:继承上下文环境的数据,拷贝一份给子线程

      package com.sxt.others;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: ThreadLocalTest4.java
       * @time: 2019/11/11 10:25
       * @desc: InheritableThreadLocal:继承上下文环境的数据,拷贝一份给子线程。起点
       */
      
      public class ThreadLocalTest4 {
          private static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
      
          public static void main(String[] args) {
              threadLocal.set(2);
              System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get());
      
              // 线程由main线程开辟
              new Thread(() -> {
                  System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get());
                  // 但是既然是拷贝,所以想改还是互不影响的
                  threadLocal.set(200);
                  System.out.println(Thread.currentThread().getName() + "-->" + threadLocal.get());
              }).start();
          }
      }
      
  • 可重入锁:锁可以延续使用 + 计数器:ReentrantLock

  • CAS(Compare and Swap)比较并交换:

    • 参考链接:CAS乐观锁

    • 悲观锁:synchronized是独占锁即悲观锁,会导致其他所有需要锁的线程挂起,等待持有锁的线程释放锁。

    • 乐观锁:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

      img

      package com.sxt.others;
      
      import java.util.concurrent.atomic.AtomicInteger;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: CASTest.java
       * @time: 2019/11/11 10:51
       * @desc: CAS
       */
      
      public class CASTest {
          // 库存
          private static AtomicInteger stock = new AtomicInteger(3);
          public static void main(String[] args){
              for (int i = 0; i < 5; i++) {
                  new Thread(()->{
                      // 模拟网络延时
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      Integer left = stock.decrementAndGet();
                      if(left<1){
                          System.out.println("抢完了...");
                          return;
                      }
                      System.out.println(Thread.currentThread().getName() + "抢了一个商品" + "-->还剩" + left);
                  }).start();
              }
          }
      }
      
  • Java 常见的锁分类及其特点JAVA锁有哪些种类,以及区别

第12章 网络编程

1. 概念

  • BS架构和CS架构的区别BS架构与CS架构的区别(全)

  • 网络:通讯协议+通信接口

  • 网络分层:OSI(Open System Interconnect)开放系统互连参考模型

    图12-1 七层协议模型.png

  • 网络分层:OSI网络通信协议模型只是一个参考模型,而TCP/IP协议是事实上的标准。TCP/IP协议参考了OSI模型,但是并没有严格按照OSI规定的七层标准去划分,而只划分了四层。

    图12-2 开放系统互连参考模型与TCPIP参考模型对比.png

  • 数据封装与解封:

  • 由于用户传输的数据一般都比较大,有的可以达到MB字节,一次性发送出去十分困难,于是就需要把数据分成许多片段,再按照一定的次序发送出去。这个过程就需要对数据进行封装。

  • 数据封装(Data Encapsulation)是指将协议数据单元(PDU)封装在一组协议头和协议尾中的过程。在OSI七层参考模型中,每层主要负责与其它机器上的对等层进行通信。该过程是在协议数据单元(PDU)中实现的,其中每层的PDU一般由本层的协议头、协议尾和数据封装构成。

  1. 数据发送处理过程

​ (1) 应用层将数据交给传输层,传输层添加上TCP的控制信息(称为TCP头部),这个数据单元称为段(Segment),加入控制信息的过程称为封装。然后,将段交给网络层。

​ (2) 网络层接收到段,再添加上IP头部,这个数据单元称为包(Packet)。然后,将包交给数据链路层。

​ (3) 数据链路层接收到包,再添加上MAC头部和尾部,这个数据单元称为帧(Frame)。然后,将帧交给物理层。

​ (4) 物理层将接收到的数据转化为比特流,然后在网线中传送。

  1. 数据接收处理过程

​ (1) 物理层接收到比特流,经过处理后将数据交给数据链路层。

​ (2) 数据链路层将接收到的数据转化为数据帧,再除去MAC头部和尾部,这个除去控制信息的过程称为解封,然后将包交给网络层。

​ (3) 网络层接收到包,再除去IP头部,然后将段交给传输层。

​ (4) 传输层接收到段,再除去TCP头部,然后将数据交给应用层。

 从以上传输过程中,可以总结出以下规则:

​ (1) 发送方数据处理的方式是从高层到底层,逐层进行数据封装。

​ (2) 接收方数据处理的方式是从底层到高层,逐层进行数据解封装。

​ 接收方的每一层只把对该层有意义的数据拿走,或者说每一层只能处理发送方同等层的数据,然后把其余的部分传递给上一层,这就是对等层通信的概念。

图12-3 数据封装.png

图12-4 数据解封.png

  • IP地址:用来标识网络中的一个通信实体的地址。通信实体可以是计算机、路由器等。 比如互联网的每个服务器都要有自己的IP地址,而每个局域网的计算机要通信也要配置IP地址。路由器是连接两个或多个网络的网络设备。

    • 目前主流使用的IP地址是IPV4,但是随着网络规模的不断扩大,IPV4面临着枯竭的危险,所以推出了IPV6。

      IPV4:32位地址,并以8位为一个单位,分成四部分,以点分十进制表示,如192.168.0.1。因为8位二进制的计数范围是00000000---11111111,对应十进制的0-255,所以-4.278.4.1是错误的IPV4地址。

      IPV6:128位(16个字节)写成8个16位的无符号整数,每个整数用四个十六进制位表示,每个数之间用冒号(:)分开,如:3ffe:3201:1401:1280:c8ff:fe4d:db39:1984

    • 注意事项

    • 127.0.0.1 本机地址

    • 192.168.0.0--192.168.255.255为私有地址,属于非注册地址,专门为组织机构内部使用。

    • InetAddress:

      1. getLocalHost:本机
      2. getByName:根据域名DNS | IP地址 --> IP
    • 两个成员方法

      • getHostAddress:返回地址
      • getHostName:返回计算机名
  • 端口:

  • IP地址用来标识一台计算机,但是一台计算机上可能提供多种网络应用程序,如何来区分这些不同的程序呢?这就要用到端口。

  • 端口是虚拟的概念,并不是说在主机上真的有若干个端口。通过端口,可以在一个主机上运行多个网络应用程序。 端口的表示是一个16位的二进制整数,对应十进制的0-65535。

  • Oracle、MySQL、Tomcat、QQ、msn、迅雷、电驴、360等网络程序都有自己的端口。

  • 查看命令

    • 查看所有端口:netstat -aon
    • 查看指定端口:netstat -aon | findstr “808”
    • 查看指定进程:tasklist | findstr “808”
    • 查看具体程序:使用任务管理器查看PID
  • 需要掌握的知识:

    1. 端口是用来区分软件的
    2. 2个字节,0-65535,UDP和TCP一样多
    3. 同一个协议端口不能冲突
    4. 定义的端口越大越好
  • InetSocketAddress

    1. 构造器 new InetSocketAddress(地址|域名, 端口);
    2. 方法:getAddress(),getPort(), getHostName()
    package com.sxt.loc;
    
    import java.net.InetSocketAddress;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: PortTest.java
     * @time: 2019/11/12 14:24
     * @desc: 端口
     */
    
    public class PortTest {
        public static void main(String[] args){
            // 包含端口
            InetSocketAddress socketAddress1 = new InetSocketAddress("127.0.0.1", 8080);
            InetSocketAddress socketAddress2 = new InetSocketAddress("localhost", 9000);
            System.out.println(socketAddress1.getHostName());
            System.out.println(socketAddress1.getAddress());
            System.out.println(socketAddress1.getPort());
            System.out.println(socketAddress2.getHostName());
            System.out.println(socketAddress2.getAddress());
            System.out.println(socketAddress2.getPort());
        }
    }
    
  • URL:

  • 在www上,每一信息资源都有统一且唯一的地址,该地址就叫URL(Uniform Resource Locator),它是www的统一资源定位符。URL由4部分组成:协议 、存放资源的主机域名、资源文件名和端口号。如果未指定该端口号,则使用协议默认的端口。例如http协议的默认端口为80。在浏览器中访问网页时,地址栏显示的地址就是URL。

  • 网络三大基石:html、http、url

  • 由4部分组成:

    1. 协议
    2. 域名、计算机
    3. 端口:默认80
    4. 请求资源
package com.sxt.loc;

import java.net.MalformedURLException;
import java.net.URL;

/**
 * @author: Li Tian
 * @contact: litian_cup@163.com
 * @software: IntelliJ IDEA
 * @file: URLTest.java
 * @time: 2019/11/14 9:27
 * @desc: URL练习
 */

public class URLTest {
    public static void main(String[] args) throws MalformedURLException {
        URL url = new URL("http://www.baidu.com:80/index.html?uname=shsxt&age=18#a");
        // 获取四个值
        System.out.println("协议:" + url.getProtocol());
        System.out.println("域名|ip:" + url.getHost());
        System.out.println("端口:" + url.getPort());
        System.out.println("请求资源1:" + url.getFile());
        System.out.println("请求资源2:" + url.getPath());

        // 参数
        System.out.println("参数:" + url.getQuery());
        // 锚点
        System.out.println("锚点:" + url.getRef());
    }
}
  • 爬虫

    • 简单爬虫

      package com.sxt.loc;
      
      import java.io.BufferedReader;
      import java.io.IOException;
      import java.io.InputStream;
      import java.io.InputStreamReader;
      import java.net.MalformedURLException;
      import java.net.URL;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: SpiderTest1.java
       * @time: 2019/11/14 10:20
       * @desc: 网络爬虫
       */
      
      public class SpiderTest1 {
          public static void main(String[] args) throws IOException {
              // 获取URL
              URL url = new URL("https://www.jd.com");
              // 下载资源
              InputStream is = url.openStream();
              BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
              String msg = null;
              while(null != (msg=br.readLine())){
                  System.out.println(msg);
              }
          }
      }
      
    • 如果爬虫被拒绝,可以模拟浏览器爬虫

      package com.sxt.loc;
      
      import java.io.BufferedReader;
      import java.io.IOException;
      import java.io.InputStream;
      import java.io.InputStreamReader;
      import java.net.HttpURLConnection;
      import java.net.MalformedURLException;
      import java.net.URL;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: SpiderTest2.java
       * @time: 2019/11/14 10:26
       * @desc: 网络爬虫,对于那些403拒绝的,模拟浏览器
       */
      
      public class SpiderTest2 {
          public static void main(String[] args) throws IOException {
              // 获取URL
              URL url = new URL("https://www.dianping.com");
              // http协议打开
              HttpURLConnection conn = (HttpURLConnection) url.openConnection();
              // 设置请求方式
              conn.setRequestMethod("GET");
              conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36");
              BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
              String msg = null;
              while (null != (msg = br.readLine())) {
                  System.out.println(msg);
              }
          }
      }
      
  • Socket:

    • 我们开发的网络应用程序位于应用层,TCP和UDP属于传输层协议,在应用层如何使用传输层的服务呢?在应用层和传输层之间,则是使用套接Socket来进行分离。

    • 套接字(Socket)就像是传输层为应用层开的一个小口,应用程序通过这个小口向远程发送数据,或者接收远程发来的数据;而这个小口以内,也就是数据进入这个口之后,或者数据从这个口出来之前,是不知道也不需要知道的,也不会关心它如何传输,这属于网络其它层次工作。

    • Socket实际是传输层供给应用层的编程接口。Socket就是应用层与传输层之间的桥梁。使用Socket编程可以开发客户机和服务器应用程序,可以在本地网络上进行通信,也可通过Internet在全球范围内通信。

      图12-5 Socket的作用.png

  • TCP协议和UDP协议的联系和区别

  • TCP协议和UDP协议是传输层的两种协议。Socket是传输层供给应用层的编程接口,所以Socket编程就分为TCP编程和UDP编程两类。

  • 在网络通讯中,TCP方式就类似于拨打电话,使用该种方式进行网络通讯时,需要建立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该数据。而UDP方式就类似于发送短信,使用这种方式进行网络通讯时,不需要建立专门的虚拟连接,传输也不是很可靠,如果发送失败则客户端无法获得。

  • 这两种传输方式都在实际的网络编程中使用,重要的数据一般使用TCP方式进行数据传输,而大量的非核心数据则可以通过UDP方式进行传递,在一些程序中甚至结合使用这两种方式进行数据传递。

  • 由于TCP需要建立专用的虚拟连接以及确认传输是否正确,所以使用TCP方式的速度稍微慢一些,而且传输时产生的数据量要比UDP稍微大一些。

  • 总结

    1. TCP是面向连接的,传输数据安全,稳定,效率相对较低。

    2. UDP是面向无连接的,传输数据不安全,效率较高。

2. UDP编程

  • 接收端

    1. 使用DatagramSocket指定端口,创建接收端
    2. 准备容器,封装成DatagramPacket包裹
    3. 阻塞式接受包裹receeive(DatagramPacket p)
    4. 分析数据,byte[] getData,getLength()
    5. 释放资源
    package com.sxt.udp;
    
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: UDPServer.java
     * @time: 2019/11/14 14:14
     * @desc: 接收端
     */
    
    public class UDPServer {
        public static void main(String[] args) throws Exception{
            System.out.println("接收方启动中...");
            //  1. 使用DatagramSocket指定端口,创建接收端
            DatagramSocket server = new DatagramSocket(9999);
            //  2. 准备容器,封装成DatagramPacket包裹
            byte[] container = new byte[1024*60];
            DatagramPacket packet = new DatagramPacket(container, 0, container.length);
            //  3. 阻塞式接受包裹receeive(DatagramPacket p)
            //  阻塞式
            server.receive(packet);
            //  4. 分析数据,byte[] getData,getLength()
            byte[] datas = packet.getData();
            int len = packet.getLength();
            System.out.println(new String(datas, 0, len));
            //  5. 释放资源
            server.close();
        }
    }
    
  • 发送端

    1. 使用DatagramSocket指定端口,创建发送端
    2. 准备数据,一定转成字节数组
    3. 封装成DatagramPacket包裹,需要指定目的地
    4. 发送包裹send(DatagramPacket p)
    5. 释放资源
    package com.sxt.udp;
    
    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.net.InetSocketAddress;
    import java.net.SocketException;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: UDPClient.java
     * @time: 2019/11/14 14:14
     * @desc: 发送端
     */
    
    public class UDPClient {
        public static void main(String[] args) throws IOException {
            System.out.println("发送方启动中...");
            //  1. 使用DatagramSocket指定端口,创建发送端
            DatagramSocket client = new DatagramSocket(8888);
            //  2. 准备数据,一定转成字节数组
            String data = "上海尚学堂";
            byte[] datas = data.getBytes();
            //  3. 封装成DatagramPacket包裹,需要指定目的地
            DatagramPacket packet = new DatagramPacket(datas, 0, datas.length, new InetSocketAddress("localhost", 9999));
            //  4. 发送包裹send(DatagramPacket p)
            client.send(packet);
            //  5. 释放资源
            client.close();
        }
    }
    
  • 注意:Address already in use: Cannot bind,同一个协议下端口不允许冲突

  • 操作基本数据类型使用Data流

    • 接收端

      package com.sxt.udp;
      
      import java.io.BufferedInputStream;
      import java.io.ByteArrayInputStream;
      import java.io.DataInputStream;
      import java.net.DatagramPacket;
      import java.net.DatagramSocket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: UDPServer.java
       * @time: 2019/11/14 14:14
       * @desc: 接收端
       */
      
      public class UDPTypeServer {
          public static void main(String[] args) throws Exception{
              System.out.println("接收方启动中...");
              //  1. 使用DatagramSocket指定端口,创建接收端
              DatagramSocket server = new DatagramSocket(9999);
              //  2. 准备容器,封装成DatagramPacket包裹
              byte[] container = new byte[1024*60];
              DatagramPacket packet = new DatagramPacket(container, 0, container.length);
              //  3. 阻塞式接受包裹receeive(DatagramPacket p)
              //  阻塞式
              server.receive(packet);
              //  4. 分析数据,将字节数组还原为对应的类型即可
              byte[] datas = packet.getData();
              int len = packet.getLength();
              DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(datas)));
              // 顺序与写出一致
              String msg = dis.readUTF();
              boolean flag = dis.readBoolean();
              System.out.println(msg + "-->" + flag);
              //  5. 释放资源
              server.close();
          }
      }
      
    • 发送端

      package com.sxt.udp;
      
      import java.io.BufferedOutputStream;
      import java.io.ByteArrayOutputStream;
      import java.io.DataOutputStream;
      import java.io.IOException;
      import java.net.DatagramPacket;
      import java.net.DatagramSocket;
      import java.net.InetSocketAddress;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: UDPClient.java
       * @time: 2019/11/14 14:14
       * @desc: 发送端
       */
      
      public class UDPTypeClient {
          public static void main(String[] args) throws IOException {
              System.out.println("发送方启动中...");
              //  1. 使用DatagramSocket指定端口,创建发送端
              DatagramSocket client = new DatagramSocket(8888);
              //  2. 将基本类型,转成字节数组
              ByteArrayOutputStream baos = new ByteArrayOutputStream();
              DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(baos));
              // 操作类型+数据
              dos.writeUTF("上海尚学堂");
              dos.writeBoolean(false);
              dos.flush();
              byte[] datas = baos.toByteArray();
              //  3. 封装成DatagramPacket包裹,需要指定目的地
              DatagramPacket packet = new DatagramPacket(datas, 0, datas.length, new InetSocketAddress("localhost", 9999));
              //  4. 发送包裹send(DatagramPacket p)
              client.send(packet);
              //  5. 释放资源
              client.close();
          }
      }
      
  • 操作引用数据类型使用Object流

  • 操作文件通过将文件转换成字节数组

  • 实现多次交流,单方面聊天

    • 发送端:

      package com.sxt.udp;
      
      import java.io.BufferedReader;
      import java.io.IOException;
      import java.io.InputStreamReader;
      import java.net.DatagramPacket;
      import java.net.DatagramSocket;
      import java.net.InetSocketAddress;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: UDPClient.java
       * @time: 2019/11/14 14:14
       * @desc: 发送端
       */
      
      public class UDPTalkClient {
          public static void main(String[] args) throws IOException {
              System.out.println("发送方启动中...");
              //  1. 使用DatagramSocket指定端口,创建发送端
              DatagramSocket client = new DatagramSocket(8888);
              //  2. 准备数据,一定转成字节数组
              BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
              while (true) {
                  String data = reader.readLine();
                  byte[] datas = data.getBytes();
                  //  3. 封装成DatagramPacket包裹,需要指定目的地
                  DatagramPacket packet = new DatagramPacket(datas, 0, datas.length, new InetSocketAddress("localhost", 9999));
                  //  4. 发送包裹send(DatagramPacket p)
                  client.send(packet);
                  if (data.equals("q")) {
                      break;
                  }
              }
              //  5. 释放资源
              client.close();
          }
      }
      
    • 接收端:

      package com.sxt.udp;
      
      import java.net.DatagramPacket;
      import java.net.DatagramSocket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: UDPServer.java
       * @time: 2019/11/14 14:14
       * @desc: 接收端
       */
      
      public class UDPTalkServer {
          public static void main(String[] args) throws Exception {
              System.out.println("接收方启动中...");
              //  1. 使用DatagramSocket指定端口,创建接收端
              DatagramSocket server = new DatagramSocket(9999);
              while (true) {
                  //  2. 准备容器,封装成DatagramPacket包裹
                  byte[] container = new byte[1024 * 60];
                  DatagramPacket packet = new DatagramPacket(container, 0, container.length);
                  //  3. 阻塞式接受包裹receeive(DatagramPacket p)
                  //  阻塞式
                  server.receive(packet);
                  //  4. 分析数据,byte[] getData,getLength()
                  byte[] datas = packet.getData();
                  int len = packet.getLength();
                  String data = new String(datas, 0, len);
                  System.out.println(data);
                  if (data.equals("q")) {
                      break;
                  }
              }
              //  5. 释放资源
              server.close();
          }
      }
      
  • 在线咨询

    • 发送端:

      package com.sxt.udp;
      
      import java.io.BufferedReader;
      import java.io.IOException;
      import java.io.InputStreamReader;
      import java.net.DatagramPacket;
      import java.net.DatagramSocket;
      import java.net.InetSocketAddress;
      import java.net.SocketException;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: TalkSend.java
       * @time: 2019/11/16 20:03
       * @desc: 使用面向对象封装
       */
      
      public class TalkSend implements Runnable {
          private DatagramSocket client;
          private BufferedReader reader;
          private String toIP;
          private int toPort;
      
          public TalkSend(int port, String toIP, int toPort) {
              this.toIP = toIP;
              this.toPort = toPort;
              try {
                  client = new DatagramSocket(port);
                  reader = new BufferedReader((new InputStreamReader(System.in)));
              } catch (SocketException e) {
                  e.printStackTrace();
              }
          }
      
          @Override
          public void run() {
              while (true) {
                  String data = null;
                  try {
                      data = reader.readLine();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
                  byte[] datas = data.getBytes();
                  //  3. 封装成DatagramPacket包裹,需要指定目的地
                  DatagramPacket packet = new DatagramPacket(datas, 0, datas.length, new InetSocketAddress(this.toIP, this.toPort));
                  //  4. 发送包裹send(DatagramPacket p)
                  try {
                      client.send(packet);
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
                  if (data.equals("q")) {
                      break;
                  }
              }
              //  5. 释放资源
              client.close();
          }
      }
      
    • 接收端

      package com.sxt.udp;
      
      import java.io.IOException;
      import java.net.DatagramPacket;
      import java.net.DatagramSocket;
      import java.net.SocketException;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: TalkReceive.java
       * @time: 2019/11/16 20:11
       * @desc: 封装接收器
       */
      
      public class TalkReceive implements Runnable {
          private DatagramSocket server;
          private String from;
      
          public TalkReceive(int port, String from) {
              this.from = from;
              try {
                  server = new DatagramSocket(port);
              } catch (SocketException e) {
                  e.printStackTrace();
              }
          }
      
          @Override
          public void run() {
              while (true) {
                  //  2. 准备容器,封装成DatagramPacket包裹
                  byte[] container = new byte[1024 * 60];
                  DatagramPacket packet = new DatagramPacket(container, 0, container.length);
                  //  3. 阻塞式接受包裹receeive(DatagramPacket p)
                  //  阻塞式
                  try {
                      server.receive(packet);
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
                  //  4. 分析数据,byte[] getData,getLength()
                  byte[] datas = packet.getData();
                  int len = packet.getLength();
                  String data = new String(datas, 0, len);
                  System.out.println(from + "说:" + data);
                  if (data.equals("q")) {
                      break;
                  }
              }
              //  5. 释放资源
              server.close();
          }
      }
      
    • 模拟学生

      package com.sxt.udp;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: TalkStudent.java
       * @time: 2019/11/16 20:13
       * @desc: 模拟学生端
       */
      
      public class TalkStudent {
          public static void main(String[] args) {
              System.out.println("学生加入聊天室...");
              new Thread(new TalkSend(7777, "localhost", 9999)).start();
              new Thread(new TalkReceive(8888, "老师")).start();
          }
      }
      
    • 模拟老师

      package com.sxt.udp;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: TalkTeacher.java
       * @time: 2019/11/16 20:13
       * @desc: 模拟老师端
       */
      
      public class TalkTeacher {
          public static void main(String[] args) {
              System.out.println("老师加入聊天室...");
              new Thread(new TalkReceive(9999, "学生")).start();
              new Thread(new TalkSend(5555, "localhost", 8888)).start();
          }
      }
      

3. TCP编程

  • 创建服务器

    1. 指定端口,使用ServerSocket创建服务器
    2. 阻塞式等待连接 accept
    3. 操作:输入输出流操作
    4. 释放资源
  • 创建客户端

    1. 建立连接:使用Socket创建客户端 + 服务的地址和端口
    2. 操作:输入输出流操作
    3. 释放资源
  • 基本步骤

    • 服务器

      package com.sxt.tcp;
      
      import java.io.DataInputStream;
      import java.io.IOException;
      import java.net.ServerSocket;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: Server.java
       * @time: 2019/11/18 14:45
       * @desc: 熟悉流程,创建服务器
       */
      
      public class Server {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Server-----");
              //  1. 指定端口,使用ServerSocket创建服务器
              ServerSocket server = new ServerSocket(8888);
              //  2. 阻塞式等待连接 accept
              Socket client = server.accept();
              System.out.println("一个客户端建立了连接...");
              //  3. 操作:输入输出流操作
              DataInputStream dis = new DataInputStream(client.getInputStream());
              String data = dis.readUTF();
              System.out.println(data);
              //  4. 释放资源
              dis.close();
              client.close();
              //  关闭服务器的话
              server.close();
          }
      }
      
    • 客户端

      package com.sxt.tcp;
      
      import java.io.DataOutputStream;
      import java.io.IOException;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: Client.java
       * @time: 2019/11/18 14:50
       * @desc: 创建客户端
       */
      
      public class Client {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Client-----");
              //  1. 建立连接:使用Socket创建客户端 + 服务的地址和端口
              Socket client = new Socket("localhost", 8888);
              //  2. 操作:输入输出流操作
              DataOutputStream dos = new DataOutputStream(client.getOutputStream());
              String data = "Hello";
              dos.writeUTF(data);
              dos.flush();
              //  3. 释放资源
              dos.close();
              client.close();
          }
      }
      
  • 模拟登陆 单向

    • 服务器

      package com.sxt.tcp;
      
      import java.io.DataInputStream;
      import java.io.IOException;
      import java.net.ServerSocket;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: LoginServer.java
       * @time: 2019/11/18 15:13
       * @desc: 模拟登陆 单向 服务器
       */
      
      public class LoginServer {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Server-----");
              //  1. 指定端口,使用ServerSocket创建服务器
              ServerSocket server = new ServerSocket(8888);
              //  2. 阻塞式等待连接 accept
              Socket client = server.accept();
              System.out.println("一个客户端建立了连接...");
              //  3. 操作:输入输出流操作
              DataInputStream dis = new DataInputStream(client.getInputStream());
              String datas = dis.readUTF();
              //  分析
              String[] dataArray = datas.split("&");
              for(String info: dataArray){
                  String[] userInfo = info.split("=");
                  System.out.println(userInfo[0] + "-->" + userInfo[1]);
              }
              //  4. 释放资源
              dis.close();
              client.close();
              //  关闭服务器的话
              server.close();
          }
      }
      
    • 客户端

      package com.sxt.tcp;
      
      import java.io.BufferedReader;
      import java.io.DataOutputStream;
      import java.io.IOException;
      import java.io.InputStreamReader;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: LoginClient.java
       * @time: 2019/11/18 15:13
       * @desc: 模拟登陆 单向 客户端
       */
      
      public class LoginClient {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Client-----");
              BufferedReader console = new BufferedReader(new InputStreamReader((System.in)));
              System.out.println("请输入用户名:");
              String uname = console.readLine();
              System.out.println("请输入密码:");
              String upwd = console.readLine();
              //  1. 建立连接:使用Socket创建客户端 + 服务的地址和端口
              Socket client = new Socket("localhost", 8888);
              //  2. 操作:输入输出流操作
              DataOutputStream dos = new DataOutputStream(client.getOutputStream());
              dos.writeUTF("uname=" + uname + "&upwd=" + upwd);
              dos.flush();
              //  3. 释放资源
              dos.close();
              client.close();
          }
      }
      
  • 模拟登陆 双向

    • 服务器

      package com.sxt.tcp;
      
      import java.io.DataInputStream;
      import java.io.DataOutputStream;
      import java.io.IOException;
      import java.net.ServerSocket;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: LoginTwoWayServer.java
       * @time: 2019/11/18 15:23
       * @desc: 模拟登陆 双向 服务器
       */
      
      public class LoginTwoWayServer {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Server-----");
              //  1. 指定端口,使用ServerSocket创建服务器
              ServerSocket server = new ServerSocket(8888);
              //  2. 阻塞式等待连接 accept
              Socket client = server.accept();
              System.out.println("一个客户端建立了连接...");
              //  3. 操作:输入输出流操作
              DataInputStream dis = new DataInputStream(client.getInputStream());
              String datas = dis.readUTF();
              String uname = "";
              String upwd = "";
      
              //  分析
              String[] dataArray = datas.split("&");
              for (String info : dataArray) {
                  String[] userInfo = info.split("=");
                  if (userInfo[0].equals("uname")) {
                      System.out.println("你的用户名为:" + userInfo[1]);
                      uname = userInfo[1];
                  } else if (userInfo[0].equals("upwd")) {
                      System.out.println("你的密码为:" + userInfo[1]);
                      upwd = userInfo[1];
                  }
              }
      
              // 输出
              DataOutputStream dos = new DataOutputStream(client.getOutputStream());
              if (uname.equals("litian") && upwd.equals("123")) {
                  dos.writeUTF("登陆成功,欢迎回来!");
              } else {
                  dos.writeUTF("登陆失败,用户名或密码错误!");
              }
              dos.flush();
      
              //  4. 释放资源
              dis.close();
              client.close();
              //  关闭服务器的话
              server.close();
          }
      }
      
    • 客户端

      package com.sxt.tcp;
      
      import java.io.*;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: LoginTwoWayClient.java
       * @time: 2019/11/18 15:23
       * @desc: 模拟登陆 双向 客户端
       */
      
      public class LoginTwoWayClient {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Client-----");
              BufferedReader console = new BufferedReader(new InputStreamReader((System.in)));
              System.out.println("请输入用户名:");
              String uname = console.readLine();
              System.out.println("请输入密码:");
              String upwd = console.readLine();
              //  1. 建立连接:使用Socket创建客户端 + 服务的地址和端口
              Socket client = new Socket("localhost", 8888);
              //  2. 操作:输入输出流操作
              DataOutputStream dos = new DataOutputStream(client.getOutputStream());
              dos.writeUTF("uname=" + uname + "&upwd=" + upwd);
              dos.flush();
      
              // 接受
              DataInputStream dis = new DataInputStream(client.getInputStream());
              String result = dis.readUTF();
              System.out.println(result);
      
              //  3. 释放资源
              dos.close();
              client.close();
          }
      }
      
  • 文件上传

    • 服务器

      package com.sxt.tcp;
      
      import java.io.*;
      import java.net.ServerSocket;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: FileServer.java
       * @time: 2019/11/18 15:32
       * @desc: 服务器:存储文件
       */
      
      public class FileServer {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Server-----");
              //  1. 指定端口,使用ServerSocket创建服务器
              ServerSocket server = new ServerSocket(8888);
              //  2. 阻塞式等待连接 accept
              Socket client = server.accept();
              System.out.println("一个客户端建立了连接...");
              //  3. 操作:文件拷贝 存储
              InputStream is = new BufferedInputStream(client.getInputStream());
              OutputStream os = new BufferedOutputStream(new FileOutputStream("./快乐保存.jpg"));
              byte[] flush = new byte[1024];
              int len = -1;
              while ((len = is.read(flush)) != -1) {
                  os.write(flush, 0, len);
              }
              //  4. 释放资源
              os.close();
              is.close();
              client.close();
              //  关闭服务器的话
              server.close();
          }
      }
      
    • 客户端

      package com.sxt.tcp;
      
      import java.io.*;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: FileClient.java
       * @time: 2019/11/18 15:32
       * @desc: 客户端:上传文件
       */
      
      public class FileClient {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Client-----");
              //  1. 建立连接:使用Socket创建客户端 + 服务的地址和端口
              Socket client = new Socket("localhost", 8888);
              //  2. 操作:文件拷贝 上传
              InputStream is = new BufferedInputStream(new FileInputStream("./快乐.jpg"));
              OutputStream os = new BufferedOutputStream(client.getOutputStream());
              byte[] flush = new byte[1024];
              int len = -1;
              while ((len = is.read(flush)) != -1) {
                  os.write(flush, 0, len);
              }
              //  3. 释放资源
              os.close();
              is.close();
              client.close();
          }
      }
      
  • 多用户登陆

    • 服务器

      package com.sxt.tcp;
      
      import java.io.DataInputStream;
      import java.io.DataOutputStream;
      import java.io.IOException;
      import java.net.ServerSocket;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: LoginMultiServer.java
       * @time: 2019/11/19 9:18
       * @desc:
       */
      
      public class LoginMultiServer {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Server-----");
              //  1. 指定端口,使用ServerSocket创建服务器
              ServerSocket server = new ServerSocket(8888);
              boolean isRunning = true;
              while (isRunning) {
                  //  2. 阻塞式等待连接 accept
                  Socket client = server.accept();
                  System.out.println("一个客户端建立了连接...");
                  new Thread(new Channel(client)).start();
              }
              //  关闭服务器的话
              server.close();
          }
      
          static class Channel implements Runnable {
              private Socket client;
              // 输入流封装
              private DataInputStream dis;
              // 输出流封装
              private DataOutputStream dos;
      
              public Channel(Socket client) {
                  this.client = client;
                  try {
                      dis = new DataInputStream(client.getInputStream());
                      dos = new DataOutputStream(client.getOutputStream());
                  } catch (IOException e) {
                      release();
                  }
              }
      
              @Override
              public void run() {
                  //  3. 操作:输入输出流操作
                  String uname = "";
                  String upwd = "";
                  //  分析
                  String datas = receive();
                  String[] dataArray = datas.split("&");
                  for (String info : dataArray) {
                      String[] userInfo = info.split("=");
                      if (userInfo[0].equals("uname")) {
                          System.out.println("你的用户名为:" + userInfo[1]);
                          uname = userInfo[1];
                      } else if (userInfo[0].equals("upwd")) {
                          System.out.println("你的密码为:" + userInfo[1]);
                          upwd = userInfo[1];
                      }
                  }
                  if (uname.equals("litian") && upwd.equals("123")) {
                      send("登陆成功,欢迎回来!");
                  } else {
                      send("登陆失败,用户名或密码错误!");
                  }
      
                  release();
              }
      
              // 接受数据
              private String receive() {
                  String datas = "";
                  try {
                      datas = dis.readUTF();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
                  return datas;
              }
      
              // 发送数据
              private void send(String msg) {
                  try {
                      dos.writeUTF(msg);
                      dos.flush();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
      
              // 释放资源
              private void release() {
                  //  4. 释放资源
                  try {
                      if (null != dos) {
                          dos.close();
                      }
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
                  try {
                      if (null != dis) {
                          dis.close();
                      }
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
                  try {
                      if (null != client) {
                          client.close();
                      }
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
      
    • 客户端

      package com.sxt.tcp;
      
      import java.io.*;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: LoginMultiClient.java
       * @time: 2019/11/19 9:18
       * @desc:
       */
      
      public class LoginMultiClient {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Client-----");
              //  1. 建立连接:使用Socket创建客户端 + 服务的地址和端口
              Socket client = new Socket("localhost", 8888);
              //  2. 操作:输入输出流操作
              new Send(client).send();
              new Receive(client).receive();
              //  3. 释放资源
              client.close();
          }
      
          static class Send {
              private Socket client;
              private DataOutputStream dos;
              private BufferedReader console;
              private String msg;
      
              public Send(Socket client) {
                  console = new BufferedReader(new InputStreamReader((System.in)));
                  this.client = client;
                  this.msg = init();
                  try {
                      dos = new DataOutputStream(client.getOutputStream());
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
      
              private String init() {
                  try {
                      System.out.println("请输入用户名:");
                      String uname = console.readLine();
                      System.out.println("请输入密码:");
                      String upwd = console.readLine();
                      return "uname=" + uname + "&upwd=" + upwd;
                  } catch (IOException e) {
                      e.printStackTrace();
                      return "???";
                  }
              }
      
              public void send() {
                  try {
                      dos.writeUTF(msg);
                      dos.flush();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          }
      
          static class Receive {
              private Socket client;
              private DataInputStream dis;
      
              public Receive(Socket client) {
                  this.client = client;
                  try {
                      dis = new DataInputStream(client.getInputStream());
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
      
              public void receive() {
                  String result = null;
                  try {
                      result = dis.readUTF();
                      System.out.println(result);
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
      

4. 在线聊天室

  • 实现一个客户可以正常收发信息

    • 服务端

      package com.sxt.chat1;
      
      import java.io.DataInputStream;
      import java.io.DataOutputStream;
      import java.io.IOException;
      import java.net.ServerSocket;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: Chat.java
       * @time: 2019/11/19 10:45
       * @desc: 在线聊天室:服务端
       * 目标:实现一个客户可以正常收发信息
       */
      
      public class Chat {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Server-----");
              //  1. 指定端口,使用ServerSocket创建服务器
              ServerSocket server = new ServerSocket(8888);
              //  2. 阻塞式等待连接 accept
              Socket client = server.accept();
              System.out.println("一个客户端建立了连接...");
              //  3. 接收消息
              DataInputStream dis = new DataInputStream(client.getInputStream());
              String msg = dis.readUTF();
              //  4. 返回消息
              DataOutputStream dos = new DataOutputStream(client.getOutputStream());
              dos.writeUTF(msg);
              dos.flush();
              //  5. 释放资源
              dos.close();
              dis.close();
              client.close();
          }
      }
      
    • 客户端

      package com.sxt.chat1;
      
      import java.io.*;
      import java.net.ServerSocket;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: Client.java
       * @time: 2019/11/19 10:45
       * @desc: 在线聊天室:客户端
       * 目标:实现一个客户可以正常收发信息
       */
      
      public class Client {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Client-----");
              //  1. 建立连接:使用Socket创建客户端 + 服务的地址和端口
              Socket client = new Socket("localhost", 8888);
              //  2. 客户端发送消息
              BufferedReader console = new BufferedReader(new InputStreamReader((System.in)));
              String msg = console.readLine();
              DataOutputStream dos = new DataOutputStream(client.getOutputStream());
              dos.writeUTF(msg);
              dos.flush();
              //  3. 获取消息
              DataInputStream dis = new DataInputStream(client.getInputStream());
              msg = dis.readUTF();
              System.out.println(msg);
              //  4. 释放资源
              dos.close();
              dis.close();
              client.close();
          }
      }
      
  • 实现一个客户可以正常收发多人信息——基础简易版

    • 服务端

      package com.sxt.chat1;
      
      import java.io.DataInputStream;
      import java.io.DataOutputStream;
      import java.io.IOException;
      import java.net.ServerSocket;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: MultiChat.java
       * @time: 2019/11/19 14:57
       * @desc: 在线聊天室:服务端
       * 目标:实现一个客户可以正常收发多人信息
       */
      
      public class MultiChat {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Server-----");
              //  1. 指定端口,使用ServerSocket创建服务器
              ServerSocket server = new ServerSocket(8888);
              //  2. 阻塞式等待连接 accept
              Socket client = server.accept();
              System.out.println("一个客户端建立了连接...");
      
              DataInputStream dis = new DataInputStream(client.getInputStream());
              DataOutputStream dos = new DataOutputStream(client.getOutputStream());
              boolean isRunning = true;
              while (isRunning) {
                  //  3. 接收消息
                  String msg = dis.readUTF();
                  //  4. 返回消息
                  dos.writeUTF(msg);
                  dos.flush();
              }
              //  5. 释放资源
              dos.close();
              dis.close();
              client.close();
          }
      }
      
    • 客户端

      package com.sxt.chat1;
      
      import java.io.*;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: MultiClient.java
       * @time: 2019/11/19 14:57
       * @desc: 在线聊天室:客户端
       * 目标:实现一个客户可以正常收发多条信息
       */
      
      public class MultiClient {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Client-----");
              //  1. 建立连接:使用Socket创建客户端 + 服务的地址和端口
              Socket client = new Socket("localhost", 8888);
              //  2. 客户端发送消息
              BufferedReader console = new BufferedReader(new InputStreamReader((System.in)));
              DataOutputStream dos = new DataOutputStream(client.getOutputStream());
              DataInputStream dis = new DataInputStream(client.getInputStream());
              boolean isRunning = true;
              while (isRunning) {
                  String msg = console.readLine();
                  dos.writeUTF(msg);
                  dos.flush();
                  //  3. 获取消息
                  msg = dis.readUTF();
                  System.out.println(msg);
              }
              //  4. 释放资源
              dos.close();
              dis.close();
              client.close();
          }
      }
      
  • 使用多线程实现多个客户可以正常收发多人信息——oop封装版

    • 问题:其他客户必须等待之前的客户退出,才能继续排队

    • 工具类:释放资源

      package com.sxt.chat3;
      
      import java.io.Closeable;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: SxtUtils.java
       * @time: 2019/11/25 13:40
       * @desc: 工具类:释放资源
       */
      
      public class SxtUtils {
          public static void close(Closeable... targets){
              for (Closeable target: targets){
                  try{
                      if(null != target){
                          target.close();
                      }
                  }catch (Exception e){
                      e.printStackTrace();
                  }
              }
          }
      }
      
    • 在线聊天室:服务端

      package com.sxt.chat3;
      
      import java.io.DataInputStream;
      import java.io.DataOutputStream;
      import java.io.IOException;
      import java.net.ServerSocket;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: MultiChat.java
       * @time: 2019/11/19 14:57
       * @desc: 在线聊天室:服务端
       * 目标:封装:使用多线程实现多个客户可以正常收发多人信息
       */
      
      public class MultiChat {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Server-----");
              //  1. 指定端口,使用ServerSocket创建服务器
              ServerSocket server = new ServerSocket(8888);
              //  2. 阻塞式等待连接 accept
              while (true) {
                  Socket client = server.accept();
                  System.out.println("一个客户端建立了连接...");
                  new Thread(new Channel(client)).start();
      
              }
          }
      
          // 一个客户代表一个Channel
          static class Channel implements Runnable {
              private DataInputStream dis;
              private DataOutputStream dos;
              private Socket client;
              private boolean isRunning;
      
              public Channel(Socket client) {
                  this.client = client;
                  try {
                      dis = new DataInputStream(client.getInputStream());
                      dos = new DataOutputStream(client.getOutputStream());
                      isRunning = true;
                  } catch (IOException e) {
                      release();
                  }
              }
      
              // 接受消息
              private String receive() {
                  String msg = "";
                  try {
                      msg = dis.readUTF();
                  } catch (IOException e) {
                      release();
                  }
                  return msg;
              }
      
              // 发送消息
              private void send(String msg) {
                  try {
                      dos.writeUTF(msg);
                      dos.flush();
                  } catch (IOException e) {
                      release();
                  }
              }
      
              // 释放资源
              private void release() {
                  this.isRunning = false;
                  SxtUtils.close(dis, dos, client);
              }
      
              @Override
              public void run() {
                  while(isRunning){
                      String msg = receive();
                      if(!msg.equals("")){
                          send(msg);
                      }
                  }
              }
          }
      }
      
    • 使用多线程封装了发送端

      package com.sxt.chat3;
      
      import java.io.BufferedReader;
      import java.io.DataOutputStream;
      import java.io.IOException;
      import java.io.InputStreamReader;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: Send.java
       * @time: 2019/11/25 14:37
       * @desc: 使用多线程封装了发送端
       */
      
      public class Send implements Runnable {
          private BufferedReader console;
          private DataOutputStream dos;
          private Socket client;
          private boolean isRunning;
      
          public Send(Socket client) {
              this.client = client;
              console = new BufferedReader(new InputStreamReader(System.in));
              try {
                  dos = new DataOutputStream(client.getOutputStream());
                  isRunning = true;
              } catch (IOException e) {
                  this.release();
              }
      
          }
      
          @Override
          public void run() {
              while (isRunning) {
                  String msg = getStrFromConsole();
                  if (!msg.equals("")) {
                      send(msg);
                  }
              }
      
          }
      
          private void send(String msg) {
              try {
                  dos.writeUTF(msg);
                  dos.flush();
              } catch (IOException e) {
                  release();
              }
          }
      
          private String getStrFromConsole() {
              try {
                  return console.readLine();
              } catch (IOException e) {
                  release();
              }
              return "";
          }
      
          // 释放资源
          private void release() {
              this.isRunning = false;
              SxtUtils.close(dos, client);
          }
      }
      
    • 使用多线程封装了接收端

      package com.sxt.chat3;
      
      import java.io.DataInputStream;
      import java.io.IOException;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: Receive.java
       * @time: 2019/11/25 14:37
       * @desc: 使用多线程封装了接收端
       */
      
      public class Receive implements Runnable {
          private Socket client;
          private boolean isRunning;
          private DataInputStream dis;
      
          public Receive(Socket client) {
              this.client = client;
              try {
                  dis = new DataInputStream(client.getInputStream());
                  isRunning = true;
              } catch (IOException e) {
                  release();
              }
          }
      
          // 接受消息
          private String receive() {
              String msg = "";
              try {
                  msg = dis.readUTF();
              } catch (IOException e) {
                  release();
              }
              return msg;
          }
      
          @Override
          public void run() {
              while (isRunning) {
                  String msg = receive();
                  if (!msg.equals("")) {
                      System.out.println(msg);
                  }
              }
          }
      
          // 释放资源
          private void release() {
              this.isRunning = false;
              SxtUtils.close(dis, client);
          }
      }
      
    • 在线聊天室:客户端

      package com.sxt.chat3;
      
      import java.io.*;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: MultiClient.java
       * @time: 2019/11/19 14:57
       * @desc: 在线聊天室:客户端
       * 目标:封装:使用多线程实现多个客户可以正常收发多条信息
       */
      
      public class MultiClient {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Client-----");
              //  1. 建立连接:使用Socket创建客户端 + 服务的地址和端口
              Socket client = new Socket("localhost", 8888);
              //  2. 客户端发送消息
              new Thread(new Send(client)).start();
              new Thread(new Receive(client)).start();
          }
      }
      
  • 手写聊天室——群聊过渡版

    • 服务器

      package com.sxt.chat4;
      
      import java.io.DataInputStream;
      import java.io.DataOutputStream;
      import java.io.IOException;
      import java.net.ServerSocket;
      import java.net.Socket;
      import java.util.concurrent.CopyOnWriteArrayList;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: MultiChat.java
       * @time: 2019/11/19 14:57
       * @desc: 在线聊天室:服务端
       * 目标:加入容器实现群聊
       */
      
      public class Chat {
          private static CopyOnWriteArrayList<Channel> all = new CopyOnWriteArrayList<>();
      
          public static void main(String[] args) throws IOException {
              System.out.println("-----Server-----");
              //  1. 指定端口,使用ServerSocket创建服务器
              ServerSocket server = new ServerSocket(8888);
              //  2. 阻塞式等待连接 accept
              while (true) {
                  Socket client = server.accept();
                  System.out.println("一个客户端建立了连接...");
                  Channel c = new Channel(client);
                  // 管理所有的成员
                  all.add(c);
                  new Thread(c).start();
      
              }
          }
      
          // 一个客户代表一个Channel
          static class Channel implements Runnable {
              private DataInputStream dis;
              private DataOutputStream dos;
              private Socket client;
              private boolean isRunning;
              private String name;
      
              public Channel(Socket client) {
                  this.client = client;
                  try {
                      dis = new DataInputStream(client.getInputStream());
                      dos = new DataOutputStream(client.getOutputStream());
                      isRunning = true;
                      // 获取名称
                      this.name = receive();
                      // 欢迎你的到来
                      this.send("欢迎你的到来");
                      sendOthers(this.name + "来了shsxt聊天室", true);
                  } catch (IOException e) {
                      release();
                  }
              }
      
              // 接受消息
              private String receive() {
                  String msg = "";
                  try {
                      msg = dis.readUTF();
                  } catch (IOException e) {
                      release();
                  }
                  return msg;
              }
      
              // 发送消息
              private void send(String msg) {
                  try {
                      dos.writeUTF(msg);
                      dos.flush();
                  } catch (IOException e) {
                      release();
                  }
              }
      
              // 群聊
              private void sendOthers(String msg, boolean isSys) {
                  for(Channel other: all){
                      if(other == this){  // 自己
                          continue;
                      }
                      if(!isSys) {
                          // 群聊消息
                          other.send(this.name + "说:" + msg);
                      }else{
                          // 系统消息
                          other.send(msg);
                      }
                  }
              }
      
              // 释放资源
              private void release() {
                  this.isRunning = false;
                  SxtUtils.close(dis, dos, client);
                  // 退出
                  all.remove(this);
                  sendOthers(this.name + "离开了...", true);
              }
      
              @Override
              public void run() {
                  while (isRunning) {
                      String msg = receive();
                      if (!msg.equals("")) {
                          // send(msg);
                          sendOthers(msg, false);
                      }
                  }
              }
          }
      }
      
    • 客户端

      package com.sxt.chat4;
      
      import java.io.BufferedReader;
      import java.io.IOException;
      import java.io.InputStreamReader;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: MultiClient.java
       * @time: 2019/11/19 14:57
       * @desc: 在线聊天室:客户端
       * 目标:加入容器实现群聊
       */
      
      public class Client {
          public static void main(String[] args) throws IOException {
              System.out.println("-----Client-----");
              BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
              System.out.println("请输入用户名:");
              String name = br.readLine();
              //  1. 建立连接:使用Socket创建客户端 + 服务的地址和端口
              Socket client = new Socket("localhost", 8888);
              //  2. 客户端发送消息
              new Thread(new Send(client, name)).start();
              new Thread(new Receive(client)).start();
          }
      }
      
    • 工具类同上

    • 发送端

      package com.sxt.chat4;
      
      import java.io.BufferedReader;
      import java.io.DataOutputStream;
      import java.io.IOException;
      import java.io.InputStreamReader;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: Send.java
       * @time: 2019/11/25 14:37
       * @desc: 使用多线程封装了发送端
       */
      
      public class Send implements Runnable {
          private BufferedReader console;
          private DataOutputStream dos;
          private Socket client;
          private boolean isRunning;
          private String name;
      
          public Send(Socket client, String name) {
              this.client = client;
              this.name = name;
              console = new BufferedReader(new InputStreamReader(System.in));
              try {
                  dos = new DataOutputStream(client.getOutputStream());
                  // 发送名称
                  send(name);
                  isRunning = true;
              } catch (IOException e) {
                  this.release();
              }
      
          }
      
          @Override
          public void run() {
              while (isRunning) {
                  String msg = getStrFromConsole();
                  if (!msg.equals("")) {
                      send(msg);
                  }
              }
      
          }
      
          private void send(String msg) {
              try {
                  dos.writeUTF(msg);
                  dos.flush();
              } catch (IOException e) {
                  release();
              }
          }
      
          private String getStrFromConsole() {
              try {
                  return console.readLine();
              } catch (IOException e) {
                  release();
              }
              return "";
          }
      
          // 释放资源
          private void release() {
              this.isRunning = false;
              SxtUtils.close(dos, client);
          }
      }
      
    • 接收端

      package com.sxt.chat4;
      
      import java.io.DataInputStream;
      import java.io.IOException;
      import java.net.Socket;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: Receive.java
       * @time: 2019/11/25 14:37
       * @desc: 使用多线程封装了接收端
       */
      
      public class Receive implements Runnable {
          private Socket client;
          private boolean isRunning;
          private DataInputStream dis;
      
          public Receive(Socket client) {
              this.client = client;
              try {
                  dis = new DataInputStream(client.getInputStream());
                  isRunning = true;
              } catch (IOException e) {
                  release();
              }
          }
      
          // 接受消息
          private String receive() {
              String msg = "";
              try {
                  msg = dis.readUTF();
              } catch (IOException e) {
                  release();
              }
              return msg;
          }
      
          @Override
          public void run() {
              while (isRunning) {
                  String msg = receive();
                  if (!msg.equals("")) {
                      System.out.println(msg);
                  }
              }
          }
      
          // 释放资源
          private void release() {
              this.isRunning = false;
              SxtUtils.close(dis, client);
          }
      }
      
  • 实现私聊

    • 修改客户端为:

      package com.sxt.chat4;
      
      import java.io.DataInputStream;
      import java.io.DataOutputStream;
      import java.io.IOException;
      import java.net.ServerSocket;
      import java.net.Socket;
      import java.util.concurrent.CopyOnWriteArrayList;
      
      /**
       * @author: Li Tian
       * @contact: litian_cup@163.com
       * @software: IntelliJ IDEA
       * @file: MultiChat.java
       * @time: 2019/11/19 14:57
       * @desc: 在线聊天室:服务端
       * 目标:加入容器实现群聊
       */
      
      public class Chat {
          private static CopyOnWriteArrayList<Channel> all = new CopyOnWriteArrayList<>();
      
          public static void main(String[] args) throws IOException {
              System.out.println("-----Server-----");
              //  1. 指定端口,使用ServerSocket创建服务器
              ServerSocket server = new ServerSocket(8888);
              //  2. 阻塞式等待连接 accept
              while (true) {
                  Socket client = server.accept();
                  System.out.println("一个客户端建立了连接...");
                  Channel c = new Channel(client);
                  // 管理所有的成员
                  all.add(c);
                  new Thread(c).start();
      
              }
          }
      
          // 一个客户代表一个Channel
          static class Channel implements Runnable {
              private DataInputStream dis;
              private DataOutputStream dos;
              private Socket client;
              private boolean isRunning;
              private String name;
      
              public Channel(Socket client) {
                  this.client = client;
                  try {
                      dis = new DataInputStream(client.getInputStream());
                      dos = new DataOutputStream(client.getOutputStream());
                      isRunning = true;
                      // 获取名称
                      this.name = receive();
                      // 欢迎你的到来
                      this.send("欢迎你的到来");
                      sendOthers(this.name + "来了shsxt聊天室", true);
                  } catch (IOException e) {
                      release();
                  }
              }
      
              // 接受消息
              private String receive() {
                  String msg = "";
                  try {
                      msg = dis.readUTF();
                  } catch (IOException e) {
                      release();
                  }
                  return msg;
              }
      
              // 发送消息
              private void send(String msg) {
                  try {
                      dos.writeUTF(msg);
                      dos.flush();
                  } catch (IOException e) {
                      release();
                  }
              }
      
              // 群聊
              private void sendOthers(String msg, boolean isSys) {
                  boolean isPrivate = msg.startsWith("@");
                  if(isPrivate){
                      // 私聊
                      int idx = msg.indexOf(":");
                      // 获取目标和数据
                      String targetName = msg.substring(1, idx);
                      msg = msg.substring(idx+1);
                      for(Channel other: all){
                          if(other.name.equals(targetName)){
                              other.send(this.name + "悄悄的对你说:" + msg);
                              break;
                          }
                      }
                  }else{
                      for(Channel other: all){
                          if(other == this){  // 自己
                              continue;
                          }
                          if(!isSys) {
                              // 群聊消息
                              other.send(this.name + "说:" + msg);
                          }else{
                              // 系统消息
                              other.send(msg);
                          }
                      }
                  }
              }
      
              // 释放资源
              private void release() {
                  this.isRunning = false;
                  SxtUtils.close(dis, dos, client);
                  // 退出
                  all.remove(this);
                  sendOthers(this.name + "离开了...", true);
              }
      
              @Override
              public void run() {
                  while (isRunning) {
                      String msg = receive();
                      if (!msg.equals("")) {
                          // send(msg);
                          sendOthers(msg, false);
                      }
                  }
              }
          }
      }
      

第13章 J20飞机游戏

  • 一些常量

    package com.sxt.planegame2;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: Constant.java
     * @time: 2019/11/28 10:15
     * @desc: 定义常量的类
     */
    
    public class Constant {
        public static final int GAME_WIDTH = 500;
        public static final int GAME_HEIGHT = 500;
    }
    
  • 物体父类

    package com.sxt.planegame2;
    
    import java.awt.*;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: GameObject.java
     * @time: 2019/11/28 9:15
     * @desc:
     */
    
    public class GameObject {
        Image img;
        double x, y;
        int speed;
        int width, height;
    
        public void drawSelf(Graphics g){
            g.drawImage(img, (int)x, (int)y, null);
        }
    
        public GameObject(Image img, double x, double y) {
            this.img = img;
            this.x = x;
            this.y = y;
        }
    
    
        public GameObject(){
        }
    
        public Rectangle getRect(){
            // 返回物体所在的矩形,便于后续的碰撞检测
            return new Rectangle((int)x, (int)y, width, height);
        }
    }
    
  • 炮弹类

    package com.sxt.planegame2;
    
    import java.awt.*;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: Shell.java
     * @time: 2019/11/28 10:08
     * @desc: 炮弹类
     */
    
    public class Shell extends GameObject {
        double degree;
    
        public Shell() {
            x = 200;
            y = 200;
            width = 10;
            height = 10;
            speed = 3;
    
            degree = Math.random() * Math.PI * 2;
        }
    
        public void draw(Graphics g) {
            Color c = g.getColor();
            g.setColor(Color.yellow);
    
            g.fillOval((int) x, (int) y, width, height);
    
            // 炮弹沿着任意角度去飞
            x += speed * Math.cos(degree);
            y += speed * Math.sin(degree);
    
            // 炮弹遇到墙壁之后反弹
            if (x < 0 || x > Constant.GAME_WIDTH - width) {
                degree = Math.PI - degree;
            }
    
            if (y < 30 || y > Constant.GAME_HEIGHT - height) {
                degree = -degree;
            }
    
            g.setColor(c);
        }
    }
    
  • 飞机类

    package com.sxt.planegame2;
    
    import java.awt.*;
    import java.awt.event.KeyEvent;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: Plane.java
     * @time: 2019/11/28 9:27
     * @desc:
     */
    
    public class Plane extends GameObject {
        boolean left, up, right, down;
        boolean live = true;
    
        public Plane(Image img, double x, double y) {
            super(img, x, y);
            speed = 10;
            width = img.getWidth(null);
            height = img.getHeight(null);
        }
    
        @Override
        public void drawSelf(Graphics g) {
            if(live){
                g.drawImage(img, (int) x, (int) y, null);
    
                if (left) {
                    x -= speed;
                }
                if (right) {
                    x += speed;
                }
                if (up) {
                    y -= speed;
                }
                if (down) {
                    y += speed;
                }
            }else{
            }
        }
    
        // 按下某个键增加相应的方向
        public void addDirection(KeyEvent e) {
            switch (e.getKeyCode()) {
                case KeyEvent.VK_LEFT:
                    left = true;
                    break;
                case KeyEvent.VK_UP:
                    up = true;
                    break;
                case KeyEvent.VK_RIGHT:
                    right = true;
                    break;
                case KeyEvent.VK_DOWN:
                    down = true;
                    break;
            }
        }
    
        // 抬起某个键取消相应的方向
        public void minusDirection(KeyEvent e) {
            switch (e.getKeyCode()) {
                case KeyEvent.VK_LEFT:
                    left = false;
                    break;
                case KeyEvent.VK_UP:
                    up = false;
                    break;
                case KeyEvent.VK_RIGHT:
                    right = false;
                    break;
                case KeyEvent.VK_DOWN:
                    down = false;
                    break;
            }
        }
    }
    
  • 爆炸类

    package com.sxt.planegame2;
    
    import java.awt.*;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: Explode.java
     * @time: 2019/11/28 14:59
     * @desc: 爆炸效果
     */
    
    public class Explode {
        double x, y;
        static Image[] imgs = new Image[16];
    
        static {
            for (int i = 0; i < 16; i++) {
                imgs[i] = GameUtil.getImage("images/explode/e" + (i + 1) + ".gif");
                imgs[i].getWidth(null);
            }
        }
    
        int count;
    
        public void draw(Graphics g) {
            if (count <= 15) {
                g.drawImage(imgs[count], (int) x, (int) y, null);
                count++;
            }
        }
    
        public Explode(double x, double y) {
            this.x = x;
            this.y = y;
        }
    }
    
  • 主程序类

    package com.sxt.planegame2;
    
    import javax.swing.*;
    import java.awt.*;
    import java.awt.event.KeyAdapter;
    import java.awt.event.KeyEvent;
    import java.awt.event.WindowAdapter;
    import java.awt.event.WindowEvent;
    import java.util.Date;
    
    /**
     * @author: Li Tian
     * @contact: litian_cup@163.com
     * @software: IntelliJ IDEA
     * @file: MyGameFrame.java
     * @time: 2019/11/27 10:33
     * @desc: 飞机游戏的主窗口
     */
    
    public class GameFrame extends Frame {
    
        Image planeImg = GameUtil.getImage("images/plane.png");
        Image bg = GameUtil.getImage("images/bg.jpg");
    
        Plane plane = new Plane(planeImg, 250, 250);
        Shell[] shells = new Shell[50];
    
        Explode bao;
        Date startTime = new Date();
        Date endTime;
        int period;     // 游戏持续的时间
    
        @Override
        public void paint(Graphics g) {
            // paint是自动调用
            g.drawImage(bg, 0, 0, null);
            plane.drawSelf(g);
    
            // 画出所有的炮弹
            for (int i = 0; i < shells.length; i++) {
                shells[i].draw(g);
                boolean peng = shells[i].getRect().intersects(plane.getRect());
    
                if (peng) {
                    plane.live = false;
    
                    // 只需要生成一次爆炸效果就ok
                    if (bao == null) {
                        bao = new Explode(plane.x, plane.y);
                        endTime = new Date();
                        period = (int) ((endTime.getTime() - startTime.getTime()) / 1000);
                    }
                    bao.draw(g);
                }
    
                if(!plane.live){
                    g.setColor(Color.red);
                    Font f = new Font("宋体", Font.BOLD, 20);
                    g.setFont(f);
                    g.drawString("时间:" + period + "秒", (int) plane.x, (int) plane.y);
                }
            }
        }
    
        // 帮助我们反复重画窗口!
        class PaintThread extends Thread {
            @Override
            public void run() {
                while (true) {
                    // 重画
                    repaint();
                    try {
                        Thread.sleep(40);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        // 增加键盘监听内部类
        class KeyMonitor extends KeyAdapter {
            @Override
            public void keyPressed(KeyEvent e) {
                plane.addDirection(e);
            }
    
            @Override
            public void keyReleased(KeyEvent e) {
                plane.minusDirection(e);
            }
        }
    
        // 初始化窗口
        public void launchFrame() {
            this.setTitle("李英俊本俊的打飞机游戏");
            this.setVisible(true);
            this.setSize(Constant.GAME_WIDTH, Constant.GAME_HEIGHT);
            this.setLocation(300, 300);
    
            // 点x就关闭程序了
            this.addWindowListener(
                    new WindowAdapter() {
                        @Override
                        public void windowClosing(WindowEvent e) {
                            System.exit(0);
                        }
                    }
            );
            // 启动重画窗口的线程
            new PaintThread().start();          // 启动重画窗口的线程
            addKeyListener(new KeyMonitor());   // 给窗口增加键盘的监听
    
            // 初始化50个炮弹
            for (int i = 0; i < shells.length; i++) {
                shells[i] = new Shell();
            }
        }
    
        public static void main(String[] args) {
            GameFrame f = new GameFrame();
            f.launchFrame();
        }
    
        private Image offScreenImage = null;
    
        @Override
        public void update(Graphics g) {
            if (offScreenImage == null)
                offScreenImage = this.createImage(Constant.GAME_WIDTH, Constant.GAME_HEIGHT);//这是游戏窗口的宽度和高度
    
            Graphics gOff = offScreenImage.getGraphics();
            paint(gOff);
            g.drawImage(offScreenImage, 0, 0, null);
        }
    }
    

我的CSDN:https://blog.csdn.net/qq_21579045

我的博客园:https://www.cnblogs.com/lyjun/

我的Github:https://github.com/TinyHandsome

纸上得来终觉浅,绝知此事要躬行~

欢迎大家过来OB~

by 李英俊小朋友

posted @ 2019-11-29 10:59  李英俊小朋友  阅读(749)  评论(0编辑  收藏  举报