尚学堂JAVA基础学习笔记_1/2

尚学堂JAVA基础学习笔记_1/2

写在前面

学习链接: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"));
    
        }
    }
    

我的CSDN:https://blog.csdn.net/qq_21579045

我的博客园:https://www.cnblogs.com/lyjun/

我的Github:https://github.com/TinyHandsome

纸上得来终觉浅,绝知此事要躬行~

欢迎大家过来OB~

by 李英俊小朋友

  • 5
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李英俊小朋友

没事啦!白嫖不要紧啦!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值