JavaSE笔记 [全文字数7.1W]

JavaSE笔记

文章目录

  • JavaSE笔记
    • 第一章 初识Java
      • 1.1 java发展史
      • 1.2 特点
      • 1.3 语言特性
      • 1.4 JDK、JRE、JVM三者关系
      • 1.5 Java加载与执行
      • 1.6 DOS基本命令
      • 1.7 常用快捷键
      • 1.8 Java环境搭建
      • 1.9 注释
    • 第二章 数据相关
      • 2.1 标识符与关键字
      • 2.2 变量
        • 2.2.1 字面量
        • 2.2.2 变量
      • 2.3 数据类型
        • 2.3.1 定义
        • 2.3.2 8种基本数据类型区别
        • 2.3.3 数据类型注意事项
        • 2.3.4 字符编码
        • 2.3.5 转义字符
      • 2.4 运算符
        • 2.4.1 算数运算符
        • 2.4.2 关系运算符
        • 2.4.3 逻辑运算符
        • 2.4.4 赋值运算符
        • 2.4.5 三目运算符
        • 2.4.6 移位运算符
        • 2.4.7 字符串拼接
        • 2.4.8 优先级
    • 第三章 控制语句
      • 3.1 键盘输入语句
      • 3.2 if语句
      • 3.3 switch语句
      • 3.4 for语句
      • 3.5 while语句
      • 3.6 do while语句
    • 第四章 方法
      • 4.1 语法
      • 4.2 方法的内存层面
      • 4.3 方法重载overload
      • 4.4 方法递归
    • 第五章 面向对象
      • 5.1 类
        • 5.1.1 类和对象的关系
        • 5.1.2 类的创建
        • 5.1.3 类的实例化(创建对象)
        • 5.1.4 (内存)运行过程
        • 5.1.5 构造方法
      • 5.2 封装
        • 5.2.1 什么是封装
        • 5.2.2 封装步骤
      • 5.3 继承
        • 5.3.1 继承机制
        • 5.3.2 继承特性
        • 5.3.3 方法覆盖(重写)
      • 5.4 多态
        • 5.4.1 什么是多态
        • 5.4.2 基础语法
        • 5.4.3 instanceof运算符
      • 5.5 this和super
      • 5.6 修饰符
        • 5.6.1 权限修饰符
        • 5.6.2 特征修饰符
          • static
          • final
          • abstract(抽象类)
          • interface(接口)
      • 5.7 Object类
        • 5.7.1 toString()
        • 5.7.2 equals()
        • 5.7.3 finalize
      • 5.8 包和import
        • 5.8.1 package
        • 5.8.2 import
        • 5.8.3 JDK常用包
      • 5.9 内部类
    • 第六章 数组
      • 6.1 数组简介
        • 6.1.1 数组扩容
      • 6.2 数组常用算法
        • 6.2.1 冒泡排序
        • 6.2.2 选择排序
        • 6.2.3 数组二分法查找
      • 6.4 Arrays工具类
        • 6.4.1 Arrays.length 数组长度
        • 6.4.2 Arrays.sort 数组排序(小到大)
        • 6.4.3 Arrays.binarySerch() 二分查找
        • 6.4.4 Arrays.toString() 数组的打印
        • 6.4.5 Arrays.fill() 数组的填充
        • 6.4.6 Arrays.equals() 判断两个数组大小是否相等
        • 6.4.7 Arrays.copyOf() 数组的拷贝
    • 第七章 常用类
      • 7.1 String
        • 7.1.1 String 特点
        • 7.1.2 String 常用方法
      • 7.2 StringBuffer 和 StringBuilder
      • 7.3 包装类
      • 7.4 日期类
        • 7.4.1 Date类
        • 7.4.2 SimpleDateFormat类
        • 7.4.3 Calendar类
      • 7.5 数字类
        • 7.5.1 Math类
        • 7.5.2 BigDecimal类
      • 7.6 Random
      • 7.7 枚举
        • 7.7.1 枚举常用方法
        • 7.7.2 枚举的高级使用
        • 7.7.3 枚举中的抽象类
        • 7.7.4 枚举的其他用法
    • 第八章 异常
      • 8.1 异常的分类
        • 8.1.1 什么是Throwable
        • 8.1.2 Exception及其子类
      • 8.2 异常的捕获
        • 8.2.1 try、catch 和 finally
        • 8.2.2 获取异常信息方法
      • 8.3 throw和throws
      • 8.4 自定义异常
    • 第九章 集合
      • 9.1 集合的结构
        • 9.1.1 集合概述
        • 9.1.2 集合继承关系
        • 9.1.3 集合和数组的区别
      • 9.2 Iterable和Collection
        • 9.2.1 Iterable接口
        • 9.2.2 Collection接口
      • 9.3 List
        • 9.3.1 ArrayList(数组)
        • 9.3.2 LinkedList(双向链表)
      • 9.4 Set
        • 9.4.1 哈希表、二叉树
        • 9.4.2 HashSet(Hash 表)
        • 9.4.3 TreeSet(二叉树)
        • 9.4.4 LinkHashSet(HashSet+LinkedHashMap)
        • 9.4.5 小结
      • 9.5 Map
        • 9.5.1 HashMap
        • 9.5.2 TreeMap
      • 9.6 Collections工具类
      • 9.7 泛型
        • 9.7.1 泛型的使用
        • 9.7.2 JDK8 泛型新特性
    • 第十章 IO流
      • 10.1 IO流概述
        • 10.1.1 InputStream 字节输入流
        • 10.1.2 OutputStream 字节输出流
        • 10.1.3 Reader 字符输入流
        • 10.1.4 Writer 字符输出流
      • 10.2 文件流
        • 10.2.1 FileInputStream(文件字节输入流)
        • 10.2.2 FileOutputStream(文件字节输出流)
        • 10.2.3 FileReader(文件字符输入流)
        • 10.2.3 FileReader(文件字符输入流)
      • 10.3 缓冲流
        • 10.3.1 字节缓冲流
        • 10.3.2 字符缓冲流
      • 10.4 转换流
        • 10.4.1 InputStreamReader
        • 10.4.2 OutputStreamWriter
      • 10.5 打印流
        • 10.5.1 完成屏幕打印的重定向
        • 10.5.2 接受屏幕输入
      • 10.6 对象流
        • 10.6.1 序列化
        • 10.6.2 反序列化
        • 10.6.3 serialVersionUID
      • 10.7 File 类
        • 10.7.1 File 类常用方法
    • 第十一章 多线程
      • 11.1 多线程的基本概念
        • 11.1.1 进程简介
        • 11.1.2 线程简介
        • 11.1.3 并行与并发
        • 11.1.4 java程序的执行流程
      • 11.2 线程的生命周期
      • 11.3 两种线程实现方式
        • 11.3.1 继承 Thread 类
        • 11.3.2 实现 Runnable 接口
        • 11.3.3 两种方式比较
      • 11.4 线程调度与控制
        • 11.4.1 线程的优先级
        • 11.4.2 Thread.sleep 线程睡眠
          • sleep与wait区别
        • 11.4.3 Thread.yield 线程让步
        • 11.4.4 Thread.join 线程插入
        • 11.4.5 Thread.interrupt 线程中断
        • 11.4.6 推荐的停止线程方式
      • 11.5 Thread类常用方法
      • 11.6 线程锁
        • 11.6.1 乐观锁
        • 11.6.2 悲观锁
        • 11.6.3 自旋锁
        • 11.6.4 Synchronized 同步锁
      • 11.7 守护线程
    • 第十二章 反射
      • 12.1 反射的基本概念
      • 12.2 反射的使用
        • 12.2.1 反射使用场合
        • 12.2.2 反射API
        • 12.2.3 使用步骤
        • 12.2.4 获取class对象的3种方法
        • 12.2.5 创建对象的两种方法
      • 12.3 反射的缺点

第一章 初识Java

1.1 java发展史

​ 1990 年末,Sun 公司准备为下一代智能家电(电视机,微波炉,电话)编写一个通用的控制系统。该团队最初考虑使用C++语言,很多成员包括Sun 公司的首席科学家Bill Joy,发现C++语言在某些方面复杂,系统资源极其有限,缺少垃圾回收系统等,于是Bill Joy 决定开发一种新的语言:Oak。

​ 1992 年夏天,Green 计划已经完成新平台的部分功能,包括Green 操作系统,Oak 的程序设计语言、类库等。同年11 月,Green 计划被转成“FirstPerson 有限公司”,一个Sun 公司的全资子公司。该团队致力于创建一种高度互动的设备。

​ 1994 年夏天,互联网和浏览器的出现不仅给广大互联网的用户带来了福音,也给Oak 语言带来了新的生机。James Gosling(Java 之父)立即意识到,这是一个机会,于是对Oak 进行了小规模的改造。

​ 1994 年秋,小组中的Naughton 和Jonathan payne 完成了第一个Java 语言的网页浏览器:WebRunner。Sun 公司实验室主任Bert Sutherland 和技术总监Eric Schmidt 观看了该网页的演示并给予了高度的评价。当时Oak 这个商标已经被注册了,于是将Oak 改名为Java。

​ 1995 年初,Sun 公司发布Java 语言,Sun 公司直接把Java 放到互联网上,免费给大家使用,甚至连源代码也不保密,也放在互联网公开。几个月后,Java 成了互联网上最热门的宝贝。各种各样的小程序层出不穷,Java 终于扬眉吐气,成为了一种广为人知的编程语言。

​ 1996 年底,Flash 问世了,这是一种更加简单的动画设计软件:使用Flash 几乎无须任何编程语言知识,就可以做出丰富多彩的动画。Flash 逐渐蚕食了Java 在网页上的应用。

​ 1997 年2 月18 日,Sun 公司发布了JDK1.1,增加了即时编译器JIT。

​ 1995 年Java 诞生到1998 年底,Java 语言虽然成为了互联网上广泛使用的编程语言,但它没有找到一个准确的定位。

​ 1998 年12 月,Sun 发布了Java 历史上最重要的JDK 版本:JDK1.2。并将Java 分成了J2EE(提供了企业应用开发相关的完整解决方案)、J2SE(整个Java 技术的核心和基础)、J2ME(主要用于控制移动设备和信息家电等有限存储的设备)三个版本。

​ 2002 年2 月,Sun 发布了JDK 历史上最为成熟的版本,JDK1.4。

​ 2004 年10 月,Sun 发布了万众期待的JDK1.5。JDK1.5 增加了诸如泛型、增强的for 语句、可变数量的形参、注释、自动拆箱和装箱等。

​ 2005 年,Java 诞生十周年,J2SE/J2EE/J2ME 分别改名为:JavaSE/JavaEE/JavaME。2006 年12 月,Sun 发布了JDK1.6。

​ 2009 年4 月20 日,Oracle 甲骨文公司宣布将以每股9.5 美元的价格收购Sun。Oracle 通过收购Sun 获得了两项资产:Java 和Solaris。

​ 2007 年11 月,Google 宣布推出一款基于Linux 平台的开源手机操作系统:Android。Android使用Java 语言来开发应用程序。Android 平台的流行,让Java 语言获得了在客户端程序上大展拳脚的机会。

​ 2011 年7 月28 日,Oracle 发布了Java SE7,这次版本升级耗时将近5 年时间。引入二进制整数、支持字符串的switch 语句等。

​ 2014 年3 月18 日,Oracle 发布了Java SE 8。2017 年7 月,Oracle 发布了JavaSE 9。

​ 2018 年3 月20 日,Oracle 发布了正式版JavaSE 10。同一年9 月25 日发布了Java11。

​ 2019 年3 月19 日,Oracle 发布了Java12。

​ 2019 年 9 月,Oracle 发布了Java SE 13

​ 2020 年 3 月,Oracle 发布了Java SE 14

1.2 特点

Java分别为三部分:JavaSE、JavaEE、JavaME。三者关系如下图:

JavaSE笔记 [全文字数7.1W]_第1张图片

JavaSE 是Java 的标准版,是学习JavaEE 和JavaME 的基础,JavaEE 是企业版,JavaME 是微型版。

1.3 语言特性

简单性:Java 语言底层采用C++语言实现,相对于C++来说,Java 是简单的,在Java语言中程序员不需要再操作复杂的指针(指针的操作是很复杂的),继承方面也是只支持单继承(C++语言是一种半面向对象的编程语言,支持多继承,多继承会导致关系很复杂),在很多方面进行了简化。

面向对象:Java 中提供了封装、继承、多态等面向对象的机制。

健壮性:在C++程序当中的无用数据/垃圾数据需要编程人员手动释放,当忘记释放内存的时候,会导致内存使用率降低,影响程序的执行;在Java 语言当中这种问题得到了解决,因为Java 语言引入了自动垃圾回收机制(GC 机制),Java 程序启动了一个单独的垃圾回收线程,时刻监测内存使用情况,在特定时机会回收/释放垃圾数据,这样会让内存时刻处于最好的状态。

多线程:Java 语言支持多个线程同时并发执行,同时也提供了多线程环境下的安全机制。

可移植性/跨平台:可移植性/跨平台表示Java 语言只需要编写/编译一次,即可通过JVM处处运行。

JavaSE笔记 [全文字数7.1W]_第2张图片

Java程序在计算机的结构:

JavaSE笔记 [全文字数7.1W]_第3张图片

1.4 JDK、JRE、JVM三者关系

① JDK:JDK(Java Development Kit)是Java 语言的软件开发工具包(SDK)。它是每一个Java 软件开发人员必须安装的。JDK 安装之后,它会自带一个JRE,因为软件开发人员编写完代码之后总是要运行的。注意:如果只是在这台机器上运行Java 程序,则不需要安装JDK,只需要安装JRE 即可。

② JRE:JRE(Java Runtime Environment,Java 运行环境),运行JAVA 程序所必须的环境的集合,包含JVM 标准实现及Java 核心类库。

③ JVM:JVM 是Java Virtual Machine(Java 虚拟机)的缩写,JVM 是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM 是实现Java 语言跨平台的法宝。

JavaSE笔记 [全文字数7.1W]_第4张图片

JDK、JRE、JVM 之间存在这样的包含关系:JDK 包含JRE,JRE又包含JVM。换句话说,只要安装了JDK,JRE 和JVM 则自动就安装了。

1.5 Java加载与执行

JavaSE笔记 [全文字数7.1W]_第5张图片

写代码–>编译–>运行

① 编写java源代码,保存为“.java”结尾的文件。

② 使用“javac”命令对java源文件进码行编译,生成“.class”结尾的字节码文件。“.class”前的文件名称为类名

③ 使用“java”命令启动JVM虚拟机,JVM通过“类加载器ClassLoader”从硬盘中找到A.class文件并装载,JVM再将字节码转换为二进制码郊游操作系统执行。

1.6 DOS基本命令

  • Win + R 打开命令窗口;输入“cmd”打开DOS窗口
  • 创建目录:/mkdir xxx
  • 切换盘符:/d: or /e:
  • 打开文件夹:/cd 路径
  • 当前目录详情:/dir
  • 清屏:/cls
  • 返回上级目录:/cd …
  • 返回根目录:/cd \
  • 退出DOS:/exit
  • 删除文件:/del xx or /del *xx (删除含关键字文件)
  • 查看本机IP:/ipconfig or /ipconfig /all (更详细)
  • 测试网络状态:/ping www.abc.com -t (-t是持续检测)

1.7 常用快捷键

  • 复制:ctrl + c
  • 粘贴 ctrl + v
  • 剪切 ctrl + x
  • 保存 ctrl + s
  • 全选 ctrl + a
  • 查找 ctrl + f
  • 撤销 ctrl + z
  • 重做 ctrl + y
  • 回到行首 home键(fn + ←)
  • 回到行尾 end键(fn + →)
  • 选中一行 shift + home/end
  • 回到文件头 ctrl + home
  • 回到文件尾 ctrl + end
  • 选中一个字母 shift + ←/→
  • 选中一个单词 鼠标双击/ctrl + shift + ←/→
  • 选中一行 鼠标三击

1.8 Java环境搭建

① 安装JDK:搜索oracle官网下载并安装JDK

② 配置环境变量:在桌面上“计算机”图标上点击右键->属性->高级系统设置->环境变量。在系统变量中找到path,添加JDK的bin文件目录,例如:C:\Program Files\Java\jdk-9.0.4\bin

③ 检查Java是否可用:/java -version or /javac -version

④ 第一个Java程序HelloWorld:

​ 新建一个HelloWorld.java文件

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("HelloWorld!");
    }
}

​ 编译:/javac d:/app/xxx.java (快捷方式:/javac + 将文件拖入窗口)

​ 运行:在字节码所在目录下/java 类名

1.9 注释

第一种:单行注释

// 斜杠后面的内容会被注释

第二种:多行注释

/*xxxxxxxxx*/    //符号*之中的内容不管有多少行全部被注释

第三种:javadoc注释

/*** 这里的信息是javadoc 注释
* @author 作者名字
* @version 版本号
* @since 自从哪个版本号开始就存在了
*/
该注释可在后期生成帮助文档

第二章 数据相关

2.1 标识符与关键字

  • 标识符包括:类名、方法名、变量名、接口名、常量名… …

  • 命名规则:

    1.标识符只能由数字、字母、下划线、符号$ 组成

    2.标识符不能以数字开头

    3.关键字不能做标识符

    4.标识符严格区分的大小写

  • 命名规范:

    1.见名知意(标识符最好有实际意义)

    2.驼峰式命名(标识符中每个单词的首字母大写)

    3.类名、接口名特殊要求(首字母大写,后面的每个单词首字母大写)

    4.变量名、方法名特殊要求(首字母小写,后面的每个单词首字母大写)

    5.常量名特殊要求(全部大写,单词与单词之间下划线衔接)

  • 关键字:

    Java关键字是编程语言中事先定义具有特殊意义的单词,标识符命名不能和关键字冲突

2.2 变量

2.2.1 字面量

数据被分为:整数型、浮点型、字符型、布尔型、字符串型等。

​ 整数型:1 2 4 99 -1 -2 … …

​ 浮点型:1.3 3.33 … …

​ 布尔型:ture、false

​ 字符型:‘a’、‘b’、‘中’ … …

​ 字符串型:“aba”、“a”、“b”、“中国”、“中” … …

2.2.2 变量

变量就是存数据的盒子

内存中最基本的储存单元变量三要素:数据类型变量名

Java中,变量需先声明,再赋值才能访问,变量没有赋值,编译报错

根据位置分类:

​ 在方法体中声明的变量叫做局部变量

​ 在方法体外及类体内声明的变量叫做成员变量

变量只能作用在对应域内(花括号内,出了花括号就不认识)

2.3 数据类型

2.3.1 定义

数据类型是用来声明变量,程序在运行过程中根据不同的数据类型分配不同大小的空间。

数据分为:基本数据类型、引用数据类型

​ 第一种:基本数据类型

​ 基本数据类型又分为4大类8小种

​ 第一类:整数型

​ 第二类:浮点型

​ 第三类:布尔型

​ 第四类:字符型

​ 8小种:

​ byte,short,int,long

​ float,double

​ boolean

​ char

​ 第二种:引用数据类型

​ Java中除基本数据类型之外,剩下的都是引用数据类型

2.3.2 8种基本数据类型区别

JavaSE笔记 [全文字数7.1W]_第6张图片

十进制转为二进制:

JavaSE笔记 [全文字数7.1W]_第7张图片

二进制转为十进制:

JavaSE笔记 [全文字数7.1W]_第8张图片

2.3.3 数据类型注意事项

整数型:

  • java中整数会自动视为为int型,数值超过-2147483678~2147483647,后面要加L
  • 大容量数据类型赋值给小容量前,要强制类型转换,否则会损失精度编译报错
    • long a = 100L;
    • int b = (int)a;
  • byte型和short型在取值范围内,字面量可以直接赋值不用强制转换
  • 多种数据类型混合运算时,结果取最大容量的数据类型

浮点型:

  • 任意一个浮点型数据都比整数型容量空间大 float容量 > long容量
  • 任意一个浮点型数据都被默认当成double型数据处理

布尔型:

  • java中boolean类型只能有两个值,只能赋值为true和false(和C、C++不同)

2.3.4 字符编码

常见的字符编码有:

ASCII('a’是97,'A’是65,'0’是48)

ISO-8859-1

GB2312

GBK

GB18030

Big5

unicode(utf8 utf16 utf32)

Java中大部分团队使用的编码为utf-8

2.3.5 转义字符

常用转义字符:

制表符:\t

换行符:\n

输出符号:\ \' \" \: ... ...

转义unicode码:\u

2.4 运算符

2.4.1 算数运算符

​ 加减乘除:+ - * /

​ 取余:%

​ 自加1自减1:++ --

2.4.2 关系运算符

​ 大于:>

​ 大于或等于:>=

​ 小于:<

​ 小于或等于:<=

​ 等于:==

​ 不等于:!=

规则:所有关系运算符的运算结果都是布尔类型,不是true就是false;“==”和“=”注意区分,一个是判断,一个是赋值

2.4.3 逻辑运算符

​ 且:&

​ 或:|

​ 非:!

​ 短路与:&&

​ 短路或:||

规则:逻辑运算符两边必须是布尔类型

短路:当使用短路&&时,左边表达式为false时,右边表达式不执行;短路或||同理

JavaSE笔记 [全文字数7.1W]_第9张图片

2.4.4 赋值运算符

= += -= *= /= %=

规则:使用扩展赋值运算符不会改变运算结果类型,但是超数值范围用+=会损失精度

byte a = 10;
a += 1;    //a = (byte)(a + 1);

注:对于对象来说,= 赋值的不是对象的值,而是对象的引用

2.4.5 三目运算符

​ 语法格式:

​ 布尔表达式 ? 表达式1 : 表达式2

​ 布尔表达式结果为true时,表达式1为整条语句的结果;

​ 布尔表达式结果为false时,表达式2为整条语句的结果;

2.4.6 移位运算符

JavaSE笔记 [全文字数7.1W]_第10张图片

2.4.7 字符串拼接

​ “+” 两边是数字类型时,求和

​ “+” 任意一边是字符串类型的时候,进行字符串拼接,结果为字符串类型

2.4.8 优先级

JavaSE笔记 [全文字数7.1W]_第11张图片

第三章 控制语句

3.1 键盘输入语句

java.util.Scanner s = new java.util.Scanner(System.in);
int xx = s.nextInt();        //接收一个整数
String xx = s.next();        //接收一个字符串

3.2 if语句

JavaSE笔记 [全文字数7.1W]_第12张图片

3.3 switch语句

JavaSE笔记 [全文字数7.1W]_第13张图片

3.4 for语句

JavaSE笔记 [全文字数7.1W]_第14张图片

3.5 while语句

JavaSE笔记 [全文字数7.1W]_第15张图片

3.6 do while语句

JavaSE笔记 [全文字数7.1W]_第16张图片

第四章 方法

4.1 语法

[修饰符列表] 返回值类型 方法名(形式参数列表){
    方法体;        //由java语句构成
}

[]中括号中内容可选,不是必须的

注意事项:

​ 返回值可以是任何数据类型,不返回值时则标注void关键字

​ 形式参数可以有多个,每个参数都是局部变量,方法结束后内存释放

​ 标有static的方法,直接通过“类名.方法(实际参数)”调用,在同一个类时,“类名.”可省略

4.2 方法的内存层面

JavaSE笔记 [全文字数7.1W]_第17张图片

方法的调用称为压栈(入栈),方法的结束称为弹栈(出栈)。

当应用启动时,main方法压栈,main方法调用其他方法时,a1方法入栈,形成一层叠一层的模式。

方法结束时,必须由栈顶方法先弹栈,再依次结束。

4.3 方法重载overload

方法功能相似时,将方法名一致构成重载,使代码更加美观

调用重载方法时,Java 编译器能通过检查调用的方法的参数类型和个数选择一个恰当的方法。

条件:

​ 在同一个类

​ 方法名相同

​ 参数列表不同(个数、类型、顺序)

注意:方法重载和修饰符列表无关、和返回值类型无关,和参数变量名字无关

4.4 方法递归

方法体中自己调用自己:

public static void sum(){
    sum();
}

第五章 面向对象

术语:面向对象编程:OOP(Object-Oriented Programming)

面向对象三大特征:

封装(Encapsulation)

继承(Inheritance)

多态(Polymorphism)

什么是面向对象和面向过程:

​ 面向过程其实是最为实际的一种思考方式,就算是面向对象的方法也是含有面向过程的思想。可以说面向过程是一种基础的方法。它考虑的是实际地实现。一般的面向过程是从上往下步步求精。面向对象主要是把事物给对象化,对象包括属性与行为。当程序规模不是很大时,面向过程的方法还会体现出一种优势。因为程序的流程很清楚,按着模块与函数的方法可以很好的组织。但对于复杂而庞大的系统来说,面向过程显得就很无力了。

​ 为了帮助大家理解面向过程和面向对象,我们再来设想一个场景,假如说编写一段程序,模拟一个人抽烟的场景,采用面向过程的方式是这样的:买烟->买打火机->找能够抽烟的场合->点燃香烟->开抽,只要按照这个流程一步一步来,就可以实现抽烟场景,采用面向对象的方式关注点就不一样了,我们会想这个场景都有什么事物参与,每个事物应该有什么行为,然后将这些事物组合在一起,来描述这个场景,例如:一个会抽烟的人(对象)+香烟(对象)+打火机(对象)+允许抽烟的场所(对象),将以上4 个对象组合在一起,就实现了抽烟场景,其中采用面向对象的方式开发具有很强的扩展力,例如:人这个对象是可以更换的,打火机也是可以更换的,香烟的品牌也是可以更换的,包括抽烟的场合也是可以更换的。如果采用面向过程方式开发,一步依赖另一步,任何一步都不能变化,变化其中一步则整个软件都会受到影响。

​ 网上发现了一篇文章,说了一下OP 与OO 的不同,并且打了一个比喻,通俗易懂。有人这么形容OP 和OO 的不同:用面向过程的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭。所谓盖浇饭,北京叫盖饭,东北叫烩饭,广东叫碟头饭,就是在一碗白米饭上面浇上一份盖菜,你喜欢什么菜,你就浇上什么菜。我觉得这个比喻还是比较贴切的。蛋炒饭制作的细节,我不太清楚,因为我没当过厨师,也不会做饭,但最后的一道工序肯定是把米饭和鸡蛋混在一起炒匀。盖浇饭呢,则是把米饭和盖菜分别做好,你如果要一份红烧肉盖饭呢,就给你浇一份红烧肉;如果要一份青椒土豆盖浇饭,就给浇一份青椒土豆丝。蛋炒饭的好处就是入味均匀,吃起来香。如果恰巧你不爱吃鸡蛋,只爱吃青菜的话,那么唯一的办法就是全部倒掉,重新做一份青菜炒饭了。盖浇饭就没这么多麻烦,你只需要把上面的盖菜拨掉,更换一份盖菜就可以了。盖浇饭的缺点是入味不均,可能没有蛋炒饭那么香。到底是蛋炒饭好还是盖浇饭好呢?其实这类问题都很难回答,非要比个上下高低的话,就必须设定一个场景,否则只能说是各有所长。如果大家都不是美食家,没那么多讲究,那么从饭馆角度来讲的话,做盖浇饭显然比蛋炒饭更有优势,他可以组合出来任意多的组合,而且不会浪费。盖浇饭的好处就是"菜"“饭"分离,从而提高了制作盖浇饭的灵活性。饭不满意就换饭,菜不满意换菜。用软件工程的专业术语就是"可维护性"比较好,“饭” 和"菜"的耦合度比较低。蛋炒饭将"蛋”“饭"搅和在一起,想换"蛋”"饭"中任何一种都很困难,耦合度很高,以至于"可维护性"比较差。软件工程追求的目标之一就是可维护性,可维护性主要表现在3 个方面:可理解性、可测试性和可修改性。面向对象的好处之一就是显著的改善了软件系统的可维护性。

5.1 类

在Java中,类就是“属性+方法”。类也是一种对象。

Java核心结构:

​ 接口–类--对象

--抽象化–> 接口

--实例化–> 对象

​ 所以对象也叫做“实例

5.1.1 类和对象的关系

​ 在编程语言当中要想创建对象则必须先有类,类是现实世界当中具有共同特征的事物进行抽象形成的模板或概念。而对象是实际存在的个体。

​ 例如:“汽车”就是一个类(所有的汽车都有方向盘、发动机,这是它们的共同特征),“你家的那个汽车”就是一个真实存在的对象。或者说“明星”是一个类,“刘德华”就是一个对象。“沈腾”、“赵本山”、“宋丹丹”都是实际存在的对象,他们都属于“笑星”类,类描述事物的共同特征,那么“笑星”类都有哪些共同特征呢?笑星类都有姓名、性别、年龄等状态信息(属性),他们还有一个共同的行为就是“演出”(方法)。但当具体到某个对象上之后,我们发现姓名是不同的,性别是不同的,年龄也是不同的,演出的效果也是不同的。所以我们在访问姓名、性别、年龄的时候,必须先有笑星对象,通过真实存在的笑星对象去访问他的属性,包括“演出”的时候,只有“笑星”类是不行的,必须先有笑星对象,让笑星对象去执行“演出”这个动作。

5.1.2 类的创建

[修饰符] class 类名{
    类体 = 属性 + 方法
}
public class Student{
    String name;
    int age;
    public void say(){
        System.out.println( name + "说话了!");
    }
}

5.1.3 类的实例化(创建对象)

public static void main(String[] args){
    Student s1 = new Student();
    s1.name = "张三";
    s1.age = 18;
    System.out.println( "姓名:" + s1.name + ";年龄:" + s1.age);
}

注:“s1”中保存的是一个内存地址——对象在堆内存中的地址。一旦“s1=null”且堆内存中的对象没有其他变量连接,这个对象将会被垃圾回收器回收。此时再用s1访问对象,会空指针异常

5.1.4 (内存)运行过程

JavaSE笔记 [全文字数7.1W]_第18张图片

创建多个对象时

JavaSE笔记 [全文字数7.1W]_第19张图片

5.1.5 构造方法

语法格式:

[修饰符列表] 构造方法名(形式参数列表){
    构造方法体;
}

① 构造方法名和类名一致。

② 构造方法用来创建对象,以及完成属性初始化操作。

③ 构造方法返回值类型不需要写,写上就报错,包括void 也不能写。

④ 构造方法的返回值类型实际上是当前类的类型。

⑤ 一个类中可以定义多个构造方法,这些构造方法构成方法重载。

⑥ 当一个类中没有提供任何构造方法,系统默认提供一个无参数的构造方法,这个无参数的构造方法叫做缺省构造器,它会把所有基本类型的实列变量设为0或false把所有引⽤类型的实例变量设为null。

⑦ 当一个类中手动提供了构造方法,那么系统将不再提供无参数的构造方法,必须手动添加一个无参构造。

5.2 封装

5.2.1 什么是封装

​ 封装可以简单理解为包装, 是指利用抽象数据类型将一系列完成某特定功能的数据和基于数据的操作包装在一起,形成一个不可分割的独立体,对外只暴露少量接口供外部调用,隐藏内部实现细节,以此来保护内部的数据以及结构安全,用术语描述就是“高内聚,低耦合”。

​ 在现实世界当中我们可以看到很多事物都是封装好的,比如“鼠标”,外部有一个壳,将内部的原件封装起来,至于鼠标内部的细节是什么,我们不需要关心,只需要知道鼠标对外提供了左键、右键、滚动滑轮这三个简单的操作。对于用户来说只要知道左键、右键、滚动滑轮都能完成什么功能就行了。为什么鼠标内部的原件要在外部包装一个“壳”呢,起码内部的原件是安全的,不是吗。再如“数码相机”,外部也有一个壳,将内部复杂的结构包装起来,对外提供简单的按键,这样每个人都可以很快的学会照相了,因为它的按键很简单,另外照相机内部精密的原件也受到了壳儿的保护,不容易坏掉。

​ 根据以上的描述,可以得出封装有什么好处呢?封装之后就形成了独立实体,独立实体可以在不同的环境中重复使用,显然封装可以降低程序的耦合度,提高程序的扩展性,以及重用性或复用性,例如“鼠标”可以在A 电脑上使用,也可以在B 电脑上使用。另外封装可以隐藏内部实现细节,站在对象外部是看不到内部复杂结构的,对外只提供了简单的安全的操作入口,所以封装之后,实体更安全了。

5.2.2 封装步骤

①需要被保护的属性使用private 进行修饰

②给这个私有的属性对外提供公开的set 和get 方法

注:set和get方法都是实例方法,不能带static。不带static的方法称为实例方法,实例方法的调用必须先new对象

5.3 继承

5.3.1 继承机制

基本作用:子类继承父类,代码可以得到复用

重要作用:有了继承才有方法的覆盖和多态

将父类所有内容复制到子类中

class 类名 extends 父类名{
    类体;
}

5.3.2 继承特性

父类也叫超类(superclass)基类

子类也叫**(subclass)**、派生类扩展类

java中只支持单继承,不支持多继承,而C++中支持多继承,也就是"class B extends A,C{ }"是错误的。

但java支持间接继承。

class B extends A{
    xxx
}class C extends B{
    xxx
}
  • java中规定子类继承父类,除构造方法不能继承外,剩下的都可以继承。但是私有的属性无法在子类中直接访问
  • java中的类没有显示继承任何类,则默认继承Object类,所有类都继承自Object类,都有Object类的特征
  • System.out.println(引用) 会直接输出地址。相当于System.out.println(引用.toString)
  • "toString"是object类中的一个输出对象在堆内存的地址的方法

5.3.3 方法覆盖(重写)

  • 条件一:两个类必须有继承关系
  • 条件二:重写之后的方法必须返回值类型、方法名、形式参数列表完全一致
  • 条件三:访问权限不能更低,只能更高
  • 条件四:重写之后的方法不能比之前的方法抛出更多的异常,可以更少

注:方法覆盖只是和方法有关,和属性无关

5.4 多态

5.4.1 什么是多态

​ 多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

5.4.2 基础语法

向上转型

​ 子–>父 Animal a = new Cat

向下转型

​ 父–>子 Cat c = new Animal

​ 两种类型之间必须要有继承关系

多态指的是:

​ 父类型引用指向子类型对象

​ 包括编译和运行阶段

​ 编译阶段:静态绑定父类的方法

​ 运行阶段:动态绑定子类对象的方法

​ 多种形态

5.4.3 instanceof运算符

instanceof 可在运行阶段动态判断引用指向的对象类型

语法:

// 引用 instanceof 类型
// 例如
d instanceof Cat    // 该表达式输出结果为ture或false

Java规范:任何情况下使用向下转型时,一定要使用instanceof 运算符进行判断,避免类型转换异常

5.5 this和super

  • this

    this是一个变量,是一个引用。this保存当前对象的内存地址,指向自身。this储存在堆内存当中的对象内部。

    this只能使用在实例方法中,谁调用这个方法,this就是谁。

    使实例方法中的局部变量有意义化,把变量指向对象,可读性高。

    构造方法也可使用this指向对象

    this(实参) 只能出现在构造方法的第一行,例如:

//当new一个无参对象时,this语法将实例变量赋值为(year=1970,month=1,day=1)减少代码量
public Date(){
    /*
    this.year = 1970;
    this.month = 1;
    this.day = 1;
    */
    this(1970,1,1);
}
public Date(int year, int month, int day){
    this.year = year;
    this.month = month;
    this.day = day;
}
  • super

    严格来说,super 其实并不是一个引用,它只是一个关键字,super 代表了当前对象中从父类继承过来的那部分特征。this 指向一个独立的对象,super 并不是指向某个“独立”的对象,假设张大明是父亲,张小明是儿子,有这样一句话:大家都说张小明的眼睛、鼻子和父亲的很像。那么也就是说儿子继承了父亲的眼睛和鼻子特征,那么眼睛和鼻子肯定最终还是长在儿子的身上。假设this指向张小明,那么super 就代表张小明身上的眼睛和鼻子。换句话说super 其实是this 的一部分。如下图所示:张大明和张小明其实是两个独立的对象,两个对象内存方面没有联系,super 只是代表张小明对象身上的眼睛和鼻子,因为这个是从父类中继承过来的,在内存方面使用了super 关键字进行了标记,对于下图来说“this.眼睛”和“super.眼睛”都是访问的同一块内存空间。

    语法:

    • super. super()
  • super. 访问父类特征时,若子类也有相同特征,super. 不能省,省了会访问子类的该特征

    • super() 作用:调用父类的构造方法,代码复用
  • super() 必须在构造方法第一行,若第一行没有手动写super,系统会默认写上super()

    • super()this() 只能存在一个,this() 调用本类中的其他方法
  • 在java中调用子类构造方法时父类的构造方法一定会执行

  • this和super比较

    JavaSE笔记 [全文字数7.1W]_第20张图片

5.6 修饰符

5.6.1 权限修饰符

JavaSE笔记 [全文字数7.1W]_第21张图片

5.6.2 特征修饰符

static
  • 定义:static 是java 语言中的关键字,表示“静态的”,它可以用来修饰变量、方法、代码块等,修饰的变量叫做静态变量,修饰的方法叫做静态方法,修饰的代码块叫做静态代码块

  • 成员变量分为实例变量静态变量

    实例变量是和对象相关的,访问时采用"引用. "的方式访问。需要new对象

    静态变量是和类相关的,访问时采用"类名. "的方式访问。不需要new对象

    静态变量保存在方法区

  • 实例方法静态方法

    实例方法和对象相关,访问时用"引用.方法"。需要new对象

    静态方法和类相关,访问时用"类名.方法"。不需要new对象

  • 静态代码块

//静态代码块在类加载时执行,并且只执行一次。
//在main方法执行之前执行。
//静态代码块按照自上而下顺序执行。
static {
    java语句;
    java语句;
}
  • 实例代码块
//创建对象之后调用构造方法之前执行
{
    java语句;
    java语句;
}
final
  • final表示最终的,不可变的
  • final可修饰变量、方法、类等
  • final修饰的类无法被继承
  • final修饰的方法无法被覆盖
  • final修饰的局部变量只能赋一次值
  • final修饰的引用永远只能指向一个对象
  • final修饰的实例变量必须手动赋值,且只能赋一次值
  • static final修饰的静态变量,称为常量,常量名全部大写,单词之间下划线隔开
  • 常量和静态变量都储存在方法区,常量只能赋值一次
abstract(抽象类)
  • Java中用abstract关键字标记的类就是抽象类

  • 语法:

[修饰符列表] abstract class 类名{
    类体;
}
  • 抽象类:类与类之间有共同特征,将这些具有共同特征的类进一步抽象形成了抽象类。类本身是不存在的,所以抽象类无法创建对象
  • 抽象类是用来被继承的
  • final和abstract无法同时存在
  • 抽象类的子类也可以是抽象类
  • 抽象方法没有方法体
  • 抽象类中不一定有抽象方法,抽象方法必须出现在抽象类中
  • 一个非抽象的类继承抽象类,必须把抽象类中的抽象方法实现
  • 抽象方法的实现也就是方法重写(覆盖)
interface(接口)
  • Java中被interface声明的是接口

  • 语法:[修饰符列表] interface 接口名{ }

  • 接口我们可以看作是抽象类的一种特殊情况,在接口中只能定义抽象的方法和常量

    1)接口中的方法默认都是public abstract 的,不能更改\

    2)接口中的变量默认都是public static final 类型的,不能更改,所以必须显示的初始化

    3)接口不能被实例化,接口中没有构造函数的概念

    4)接口之间可以继承,但接口之间不能实现

    5)接口中的方法只能通过类来实现,通过implements 关键字

    6)如果一个类实现了接口,那么接口中所有的方法必须实现

    7)一类可以实现多个接口

    8)继承和实现可以共存,extends在前implements在后

  • 接口和抽象类的区别

    a)接口描述了方法的特征,不给出实现,一方面解决java 的单继承问题,实现了强大的可接插性

    b)抽象类提供了部分实现,抽象类是不能实例化的,抽象类的存在主要是可以把公共的代码移植到抽象类中

    c)面向接口编程,而不要面向具体编程(面向抽象编程,而不要面向具体编程)

    d)优先选择接口(因为继承抽象类后,此类将无法再继承,所以会丧失此类的灵活性)

5.7 Object类

Object类是所有Java的根类,如果声明类时为使用extends关键字指明其基类,则默认Object为基类

5.7.1 toString()

作用:默认情况下返回一个“以文本方式表示”此对象的字符串

通常情况下会把toString()重写

5.7.2 equals()

“==”equals()的区别:

​ 双等号可以比较基本类型和引用类型,等号比较的是值,特别是比较引用类型,比较的是引用的内存地址

​ 采用equals 比较两个对象是否相等,通常会对equals方法重写,自定义比较规则

5.7.3 finalize

垃圾回收器(Garbage Collection),也叫GC,垃圾回收器主要有以下特点:

1)当对象不再被程序使用时,垃圾回收器将会将其回收

2)垃圾回收是在后台运行的,我们无法命令垃圾回收器马上回收资源,但是我们可以告诉他,尽快回收资源【System.gcRuntime.getRuntime().gc()

3)垃圾回收器在回收某个对象的时候,首先会调用该对象的finalize 方法

4)GC 主要针对堆内存

5)单例模式的缺点

5.8 包和import

5.8.1 package

package出现在java源文件第一行

//package 域名倒叙 + 项目名 + 模块名 + 功能名
//例如
package com.aaa.bbb.ccc;

完整类名 = 包名 + 类名

5.8.2 import

调用java.lang和同包下的类不需要import,其他情况都需要import导包

语法:import 完整包名;import 包名.*;

import在package之后,class之前

5.8.3 JDK常用包

java.lang,此包Java 语言标准包,使用此包中的内容无需import 引入

java.sql,提供了JDBC 接口类

java.util,提供了常用工具类j

ava.io,提供了各种输入输出流

5.9 内部类

  • 内部类分为四种:

    实例内部类

    静态内部类

    局部内部类

    匿名内部类

  • 实例内部类:

    创建实例内部类,外部类的实例必须已经创建

    实例内部类会持有外部类的引用

    实例内部不能定义static 成员,只能定义实例成员

  • 静态内部类:

    静态内部类不会持有外部的类的引用,创建时可以不用创建外部类

    静态内部类可以访问外部的静态变量,如果访问外部类的成员变量必须通过外部类的实例访问

  • 局部内部类:

    局部内部类是在方法中定义的,它只能在当前方法中使用。和局部变量的作用一样局部内部类和实例内部类一致,不能包含静态成员

  • 匿名内部类:

    MyInterface myInterface = new MyInterface() {
        public void add() {
            System.out.println("-------add------");
        }
    };
    

第六章 数组

6.1 数组简介

  • 定义:数组是指一组数据的集合,数组中的每个数据称为元素。数组中的元素可以是任意类型(基本类型和引用类型),同一个数组里只能存放类型相同的元素

  • 一维数组

    动态初始化:int[] arr = new int[5];

    静态初始化:int[] arr = {1,2,3};

  • 二维数组

    动态初始化:int[][] arr = new int[2][3];

    静态初始化:int[][] arr = {{1,2,3},{1,2},{6,5}};

  • 数组内存层面

    JavaSE笔记 [全文字数7.1W]_第22张图片

其中data变量储存的是下标为[0]的元素的内存地址,整个数组的元素内存地址是连续的。

当数组是引用数据类型时:

JavaSE笔记 [全文字数7.1W]_第23张图片

二维数组的内存结构:

JavaSE笔记 [全文字数7.1W]_第24张图片

其中1,2,3 在内存中分别是三块不同的内存

6.1.1 数组扩容

  • Java中数组长度一旦确定则不可变。

  • 数组的扩容:

    实际是创建一个新的大容量数组,将小容量数组的数据拷贝到新数组。

  • 结论:数组的扩容因为涉及到拷贝问题,扩容效率较低,在实际开发中应尽量避免数组扩容。

6.2 数组常用算法

6.2.1 冒泡排序

案例:假设有5 个数字3,1,6,2,5 在一个int 数组中,要求按从小到大排序输出

冒泡排序的算法是这样的,首先从数组的最左边开始,取出第0 号位置(左边)的数据和第1号位置(右边)的数据,如果左边的数据大于右边的数据,则进行交换,否而不进行交换。接下来右移一个位置,取出第1 个位置的数据和第2 个位置的数据,进行比较,如果左边的数据大于右边的数据,则进行交换,否而不进行交换。沿着这个算法一直排序下去,最大的数就会冒出水面,这就是冒泡排序。

以上示例排序过程如下:

JavaSE笔记 [全文字数7.1W]_第25张图片

从上面我们看到了比较了N-1 次,那么第二遍就为N-2 次比较了,如此类推,比较次数的公式如下:

(N-1) + (N-2)+...+1=((N-1)*N)/2

所以以上总共比较次数为 ((5-1)*5)/2=10

public class ArraySortTest01 {
    public static void main(String[] args) {
        int[] data = {3,1,6,2,5};
        for (int i=data.length-1; i>0; i--) {
            for (int j=0; j<i; j++) {
                if (data[j] > data[j+1]) {
                    int temp = data[j];
                    data[j] = data[j+1];
                    data[j+1] = temp;
                }
            }
        }
        for (int i=0; i<data.length; i++) {
            System.out.println(data[i]);
        }
    }
}

6.2.2 选择排序

选择排序对冒泡排序进行了改进,使交换次数减少,但比较次数仍然没有减少。

案例:假设有5 个数字3,1,6,2,5 在一个int 数组中,要求按从小到大排序输出

采用选择排序,选择排序是这样的,先从左端开始,找到下标为0 的元素,然后和后面的元素依次比较,如果找到了比下标0 小的元素,那么再使用此元素,再接着依次比较,直到比较完成所有的元素,最后把最小的和第0 个位置交换。

以上示例排序过程如下:

JavaSE笔记 [全文字数7.1W]_第26张图片

JavaSE笔记 [全文字数7.1W]_第27张图片

第二遍排序将从下标为1 的元素开始,以此类推,经过N(N-1)/2 次比较,经过N 次数据交互就完成了所有元素的排序。

public class ArraySortTest02 {
    public static void main(String[] args) {
        int[] data = {3,1,6,2,5};
        for (int i=0; i<data.length; i++) {
            int min = i;
            for (int j=i+1; j<data.length; j++) {
                if (data[j] < data[min]) {
                    min = j;
                }
            }
            //进行位置的交换
            if (min != i) {
                int temp = data[i];
                data[i] = data[min];
                data[min] = temp;
            }
        }
        for (int i=0; i<data.length; i++) {
            System.out.println(data[i]);
        }
    }
}

6.2.3 数组二分法查找

查找数组中的元素我们可以遍历数组中的所有元素,这种方式称为线性查找。线性查找适合与小型数组,大型数组效率太低。如果一个数组已经排好序,那么我们可以采用效率比较高的二分查找或叫折半查找算法

在这里插入图片描述

假设,我们准备采用二分法取得18 在数组中的位置

第一步,首先取得数组0~9 的中间元素

​ 中间元素的位置为:(开始下标0 + 结束下标9)/2=下标4

​ 通过下标4 取得对应的值15

​ 18 大于15,那么我们在后半部分查找

第二步,取数组4~9 的中间元素

​ 4~9 的中间元素=(下标4 + 1 +下标9)/2=下标7

​ 下标7 的值为18,查找完毕,将下标7 返回即可

以上就是二分或折半查找法,此种方法必须保证数组事先是排好序的,这一点一定要注意

6.4 Arrays工具类

6.4.1 Arrays.length 数组长度

public static void returnArrayLength() {
    int[] numGroup = {25,12,68,78,33,55};
    System.out.println("输出数组长度为:" + numGroup.length);
}

6.4.2 Arrays.sort 数组排序(小到大)

public static void returnArraysSort() {
	int[] numGroup = {25,12,68,78,33,55};
	Arrays.sort(numGroup);
	for (int i : numGroup) {
		System.out.print(i+"\t");
	}
}

6.4.3 Arrays.binarySerch() 二分查找

public static void returnArraysBinarySerch() {
    int[] numGroup = {25,12,68,78,33,55};
    int index=Arrays.binarySearch(numGroup, 12);
    System.out.println("该元素下标为:"+index);
}

6.4.4 Arrays.toString() 数组的打印

public static void returnArrayToString() {
    int[] numGroup = {25,12,68,78,33,55};
    System.out.println("输出数组内容为:" + Arrays.toString(numGroup) );
}

6.4.5 Arrays.fill() 数组的填充

public static void returnArrayFill() {
    int[] numGroup = {25,12,68,78,33,55};
    Arrays.fill(numGroup, 13);
    System.out.println("数组的填充:"+Arrays.toString(numGroup));
}

6.4.6 Arrays.equals() 判断两个数组大小是否相等

public static void returnArraysEquals() {
    int[] numGroup = {25,12,68,78,33,55};
    int[] numGroup02 = {35,12,68,78,33,55};
    Boolean judge=Arrays.equals(numGroup, numGroup02);
    System.out.println("判断两个数组是否相等:"+judge);
}

6.4.7 Arrays.copyOf() 数组的拷贝

public static void returnArrayListCopyOf() {
	int[] numGroup = {25,12,68,78,33,55};
	int[] numGroup02=null;
	numGroup02=Arrays.copyOf(numGroup, 9);    //从下标0开始拷贝9个元素
	System.out.println(Arrays.toString(numGroup02));
}

第七章 常用类

7.1 String

7.1.1 String 特点

字符串不可变,它们的值在创建之后不能被更改;虽然String的值是不可变的,但是它们可以被共享

字符串的效果上相当于字符数组(char[]),但是底层原理是字节数组(byte[])

通过new创建的字符串对象,每一次new都会申请一块内存空间,虽然内容是相同的,但是地址值是不同的

以""方式给出的字符串,只要字符序列相同(顺序和大小写完全相同),无论在程序代码中出现几次,JVM都只会创建一个String对象放置于堆内存的字符串常量池中

7.1.2 String 常用方法

//返回字符串指定索引的char字符
char charAt(int index);

//判断字符串是否以指定的后缀结束
boolean endsWith(String suffix);
//判断字符串是否以指定的前缀开始
boolean startsWith(String prefix);
//判断字符串从指定的索引开始,是否以指定的前缀开始
boolean startsWith(String prefix, int toffset);

//字符串相等比较,不忽略大小写
boolean equals(Object anObject);
//字符串相等比较,忽略大小写
boolean equalsIgnoreCase(String anotherString);

//取得指定字符在字符串的位置
int indexOf(String str);
int indexOf(String str, int fromIndex);
//返回最后一次字符串出现的位置
int lastIndexOf(String str);
int lastIndexOf(String str, int fromIndex);

//取得字符串的长度
int length();

//判断是否为空字符串
boolean isEmpty();

//判断前面的字符串是否包含后面的子字符串
boolean contains(CharSequence s);

//替换字符串中指定的内容
String replace(char oldChar, char newChar);
String replaceAll(String regex, String replacement);

//根据指定的表达式拆分字符串
String[] split(String regex);
String[] split(String regex, int limit);

//截子串
String substring(int beginIndex, int endIndex);

//去前尾空格
String trim();

//将其他类型转换成字符串
static String valueOf(E e);

//将字符串转换成小写
String toLowerCase();
//将字符串转换成大写
String toUpperCase();
  • “==”与“equals”的区别

    ==:进行的是数值比较,比较的是两个字符串对象的内存地址数值。

    equals():可以进行字符串内容的比较。

7.2 StringBuffer 和 StringBuilder

  • StringBuffer

    StringBuffer 称为字符串缓冲区,它的工作原理是:预先申请一块内存,存放字符序列,如果字符序列满了,会重新改变缓存区的大小,以容纳更多的字符序列。StringBuffer 是可变对象,这个是String 最大的不同。

    //创建一个初始化容量为16个byte[] 数组。(字符串缓冲区对象)
    StringBuffer stringBuffer = new StringBuffer();
    //拼接字符串,append() 追加方法
    stringBuffer.append("a");
    stringBuffer.append(3.14);
    
  • StringBuilder

    用法同StringBuffer,StringBuilder 和StringBuffer 的区别是StringBuffer 中所有的方法都是同步的,是线程安全的,但速度慢,StringBuilder 的速度快,但不是线程安全的。

  • “+”连接符

    Java语言为“+”连接符以及对象转换为字符串提供了特殊的支持,字符串对象可以使用“+”连接其他对象。

    public static void main(String[] args) {
        int i = 10;
        String s = "abc";
        System.out.println(s + i);
    }
    

    底层实现原理:使用“+”拼接字符串时会创建一个StringBuilder()对象,并调用append()方法将数据拼接,最后调用toString()方法返回拼接好的字符串。

7.3 包装类

8种基本数据类型对应的包装类

JavaSE笔记 [全文字数7.1W]_第28张图片

类层次结构

JavaSE笔记 [全文字数7.1W]_第29张图片

自动装箱和自动拆箱机制

  • 自动将基础类型转换为对象

  • 自动将对象转换为基础类型

  • JDK5.0之前:

    装箱:Integer i1 = new Integer(100);

    拆箱:int i2 = i1.intValue();

  • JDK5.0之后:

    装箱:Integer i3 = 100;

    拆箱:Int i4 = i3;

“==” 和 equals

  • “==” 比较的是内存地址,equals 在包装类中重写了,比较的是基本数据类型的值。

    Integer i1= 10;    //10自动包装成对象,存在Integer类的静态元素区
    Integer i2=10;
    Integer i3=new Integer(10);    //放在在堆内存
    Integer i4=new Integer(10);
    System.out.println(i1==i2);    		  //true
    System.out.println(i1==i3);   		  //false
    System.out.println(i3==i4);   		  //false
    System.out.println(i1.equals(i2));    //true
    System.out.println(i1.equals(i3));    //true
    System.out.println(i3.equals(i4));    //true
    

7.4 日期类

7.4.1 Date类

  • Date类中大部分方法已经过时(不推荐使用),Calendar类作为升级版代替其作用

  • 主要用的构造方法:public Date()public Date(long date)

    其无参构造在底层用到 System.currentTimeMillis() 方法获取当前系统时间然后通过方法调用将此时间传递给了 long 型有参方法进行私有成员变量赋值

  • 获取当前系统时间:Date today = new Date();

  • 获取当前时间(毫秒):long begin = System.currentTimeMillis();

7.4.2 SimpleDateFormat类

  • 这是一个日期格式化类

  • 设置日期格式:SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");

    y 代表年

    M 代表月

    d 代表日

    E 代表星期

    H 代表24进制的小时

    h 代表12进制的小时

    m 代表分钟

    s 代表秒

    S 代表毫秒

  • 常用方法:

    • format():按照指定的格式模式格式化日期,返回一个日期字符串

      String s = sdf.format(new Date());

    • parse():用给定的参数-日期字符串来转成时间格式,返回Date对象

      Date d = sdf.parse("2008-08-08 08:08:08");

7.4.3 Calendar类

Calendar 类是一个抽象类,但是可以 getInstance() 方法获得 Calendar 对象。通过Calendar类可更加方便进行日期的计算

  • 常用方法:

    • getInstance():一般通过此方法创建Calendar对象,该方法的作用是获取当前系统时间的Calendar对象

      Calendar calendar = Calendar.getInstance();

    • getTime():获取时间日期,返回一个Date

      Date time = calendar.getTime();

    • setTime(Date date):使用给定的Date设置日历的时间

      calendar.setTime(date);

    • get():获取年月日时分秒单项日期信息

      ​ 年:int year = calendar.get(Calendar.YEAR);

      ​ 月:int month = calendar.get(Calendar.MONTH);

      ​ 日:int day= calendar.get(Calendar.DAY_OF_MONTH);

      ​ 时:int hour= calendar.get(Calendar.HOUR_OF_DAY);

      ​ 分:int minute = calendar.get(Calendar.MINUTE);

      ​ 秒:int second= calendar.get(Calendar.SECOND);

      ​ 当前年的第几天:DAY_OF_YEAR

      ​ 当前年的第几周:WEEK_OF_YEAR

      ​ 当前月的第几周:WEEK_OF_MONTH

      ​ 当前周的第几天:DAY_OF_WEEK

    • set():设置Calendar对象日期,和 get() 类似

      calendar.set(Calentdar.XXX,number);

    • add():日期调整

      ​ 3小时后:calendar.add(Calendar.HOUR_OF_DAY,3);

      ​ 15分钟前:calendar.add(Calendar.MINUTE,-15);

      ​ 7天后:calendar.add(Calendar.DAY_OF_YEAR,7);

    • getTimeZone():获取时区操作对象

      TimeZone timeZone = calendar.getTimeZone();

      System.out.println(timeZone.getID()); //Asia/Shanghai

      System.out.println(timeZone.getDisplayName()); //中国标准时间

    • 要计算时间差,可用 Calendar.getTimeInMillis() 取得两个时间的微秒级的时间差,再加以换算即可,比如获得相差天数:

      long val = calendarEnd.getTimeInMillis() - calendarBegin.getTimeInMillis();

      long day = val / (1000 * 60 * 60 * 24);

7.5 数字类

7.5.1 Math类

常用方法:

  • abs():返回给定数的绝对值,方法提供了4个不同参数类型重载方法(int, long, float, double)

    int abs1 = Math.abs(-1);
    long abs2 = Math.abs(-3l);
    float abs3 = Math.abs(-1.2f);
    double abs4 = Math.abs(-3.923);
    System.out.println(abs1);
    System.out.println(abs2);
    System.out.println(abs3);
    System.out.println(abs4);
    
  • ceil():返回大于或等于参数且等于一个数学整数的最小的双精度值,可以理解为向上取整

    System.out.println(Math.ceil(-1.3));//-1.0
    System.out.println(Math.ceil(1.9));//2.0
    System.out.println(Math.ceil(-7.9));//-7.0
    System.out.println(Math.ceil(123));//123.0
    
  • floor():返回最大的双精度值,该双精度值小于或等于参数,并且等于一个数学整数,可以理解为向下取整

    System.out.println(Math.floor(-1.3));//-2.0
    System.out.println(Math.floor(1.9));//1.0
    System.out.println(Math.floor(1.3));//1.0
    System.out.println(Math.floor(-7.9));//-8.0
    System.out.println(Math.floor(123));//123.0
    
  • round():返回与参数最接近的整型数,四舍五入为正无穷,其实就是四舍五入的整数

    System.out.println(Math.round(-1.3));//-1
    System.out.println(Math.round(1.9));//2
    System.out.println(Math.round(1.3));//1
    System.out.println(Math.round(-7.9));//-8
    System.out.println(Math.round(123));//123
    
  • max():返回最大值,该方法提供了4个不同参数类型重载方法(int,long,float,double)

    System.out.println(Math.max(1, 3));//3
    System.out.println(Math.max(-4, -5));//-4
    System.out.println(Math.max(1.8, 1.92));//1.92
    System.out.println(Math.max(-4f, -4f));//-4.0
    
  • min(a, b):返回最小值,该方法提供了4个不同参数类型重载方法(int, long, float, double)

    System.out.println(Math.min(1, 3));//1
    System.out.println(Math.min(-4, -5));//-5
    System.out.println(Math.min(1.8, 1.92));//1.8
    System.out.println(Math.min(-4f, -4f));//-4.0
    
  • pow(a, b):返回 a 的 b 次方,其中参数和返回值都是 double 类型的

    System.out.println(Math.pow(3, 3));//27.0
    System.out.println(Math.pow(3.2, 5));//335.5443200000001
    
  • random():生成一个 double 类型的随机数,范围是[ 0.0, 1.0),注意是左闭右开。

    System.out.println(Math.random());//0.4128879706448445
    System.out.println(Math.random());//0.9024029619163387
    System.out.println(Math.random());//0.4265563513755902
    

7.5.2 BigDecimal类

​ Java中提供了大数字(超过16位有效位)的操作类,即 java.math.BinInteger 类和 java.math.BigDecimal 类,用于高精度计算.其中 BigInteger 类是针对大整数的处理类,而 BigDecimal 类则是针对大小数的处理类。BigDecimal 类的实现用到了 BigInteger类,不同的是 BigDecimal 加入了小数的概念。float和Double只能用来做科学计算或者是工程计算;在商业计算中,对数字精度要求较高,必须使用 BigInteger 类和 BigDecimal 类,它支持任何精度的定点数,可以用它来精确计算货币值。

推荐使用的构造方法:

BigDecimal BigDecimal(String s);static BigDecimal valueOf(double d);

加减乘除:

public class bigDecimalTest{}
    BigDecimal add(BigDecimal augend);    //加
    BigDecimal subtract(BigDecimal subtrahend);    //减
    BigDecimal multiply(BigDecimal multiplicand);    //乘
    BigDecimal divide(BigDecimal divisor);    //除
    BigDecimal divide(BigDecimal divisor,int scale, int roundingMode);    //商,几位小数,舍取模式
    BigDecimal remainder(BigDecimal divisor);    //求余数
    BigDecimal max(BigDecimal value);    //最大值
    BigDecimal min(BigDecimal value);    //最小值
    BigDecimal abs();    //绝对值
    BigDecimal negate();    //相反数
}

舍入模式有下面这几种:

ROUND_CEILING:向正无穷方向舍入
ROUND_DOWN:向零方向舍入
ROUND_FLOOR:向负无穷方向舍入
ROUND_HALF_DOWN:向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,向下舍入,
    			 例如1.55 保留一位小数结果为1.5
ROUND_HALF_EVEN:向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,
    			 如果保留位数是奇数,使用ROUND_HALF_UP,如果是偶数,使用ROUND_HALF_DOWN
ROUND_HALF_U:向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,向上舍入, 
			  1.55保留一位小数结果为1.6
ROUND_UNNECESSARY:计算结果是精确的,不需要舍入模式
ROUND_UP:向远离0的方向舍入

数字格式化:

###,###.## 表示加入千分位,保留两个小数。
###,###.0000 表示加入千分位,保留4个小数,不够补0
DecimalFormat df = new DecimalFormat("###,###.000");
String result = df.format(54363.256321);

7.6 Random

Random类位于java.util包下,它的作用是生成一个随机数

常用方法如下:

Random random=new Random();    //以系统当前时间作为随机数生成的种子
random.nextInt(10);   		   //返回一个大于0且小于10的整数
random.nextFloat();   	  	   //返回一个随机浮点型
random.nextBoolean();  	  	   //返回一个随机布尔型值
random.nextDouble();   		   //返回一个随机双精度型
random.nextLong();   		   //返回一个随机长整形

7.7 枚举

public enum Weekday {
    SUN(0),MON(1),TUS(2),WED(3),THU(4),FRI(5),SAT(6);
    private int value;
    private Weekday(int value){
        this.value = value;
    }
    public static Weekday getNextDay(Weekday nowDay){
        int nextDayValue = nowDay.value;
        if (++nextDayValue == 7){
            nextDayValue =0;
        }
        return getWeekdayByValue(nextDayValue);
    }
    public static Weekday getWeekdayByValue(int value) {
        for (Weekday c : Weekday.values()) {
            if (c.value == value) {
                return c;
            }
        }
        return null;
    }
}
class Test2{
    public static void main(String[] args) {
        System.out.println("nowday ====> " + Weekday.SAT);
        System.out.println("nowday int ====> " + Weekday.SAT.ordinal());
        System.out.println("nextday ====> " + Weekday.getNextDay(Weekday.SAT)); // 输出 SUN
        //输出:
        //nowday ====> SAT
        //nowday int ====> 6
        //nextday ====> SUN
    }
}

在没有枚举类的时候,我们要定义一个有限的序列,比如星期几,男人女人,春夏秋冬,一般会通过静态变量的形式,但是使用那样的形式如果需要一些其他的功能,需要些很多奇奇怪怪的代码。所以,枚举类的出现,就是为了简化这种操作。

7.7.1 枚举常用方法

public enum Weekday {
    SUN,MON,TUS,WED,THU,FRI,SAT
}
class Test3{
    public static void main(String[] args) {
        System.out.println(Weekday.valueOf("mon".toUpperCase()));
        //MON
        for (Weekday w : Weekday.values()){
            System.out.println(w + ".ordinal()  ====>" +w.ordinal());
        }
        //SUN.ordinal()  ====>0
        //MON.ordinal()  ====>1
        //TUS.ordinal()  ====>2
        //WED.ordinal()  ====>3
        //THU.ordinal()  ====>4
        //FRI.ordinal()  ====>5
        //SAT.ordinal()  ====>6
        System.out.println("Weekday.MON.compareTo(Weekday.FRI) ===> " + Weekday.MON.compareTo(Weekday.FRI));
        System.out.println("Weekday.MON.compareTo(Weekday.MON) ===> " + Weekday.MON.compareTo(Weekday.MON));
        System.out.println("Weekday.MON.compareTo(Weekday.SUM) ===> " + Weekday.MON.compareTo(Weekday.SUN));
        //Weekday.MON.compareTo(Weekday.FRI) ===> -4
        //Weekday.MON.compareTo(Weekday.MON) ===> 0
        //Weekday.MON.compareTo(Weekday.SUM) ===> 1
        System.out.println("Weekday.MON.name() ====> " + Weekday.MON.name());
        //Weekday.MON.name() ====> MON
    }
}
  • Weekday.valueOf() 方法

    它的作用是传来一个字符串,然后将它转变为对应的枚举变量。前提是你传的字符串和定义枚举变量的字符串一抹一样,区分大小写。如果你传了一个不存在的字符串,那么会抛出异常。

  • Weekday.values() 方法

    这个方法会返回包括所有枚举变量的数组。在该例中,返回的就是包含了七个星期的 Weekday[] 。可以方便的用来做循环。

  • 枚举变量的toString()方法

    该方法直接返回枚举定义枚举变量的字符串,比如MON就返回【”MON”】。

  • 枚举变量的.ordinal()方法

    默认情况下,枚举类会给所有的枚举变量一个默认的次序,该次序从0开始,类似于数组的下标。而.ordinal()方法就是获取这个次序(或者说下标)

  • 枚举变量的compareTo()方法

    该方法用来比较两个枚举变量的”大小”,实际上比较的是两个枚举变量的次序,返回两个次序相减后的结果,如果为负数,就证明变量1”小于”变量2

  • 枚举类的name()方法

    它和toString()方法的返回值一样,唯一的区别是,你可以重写toString方法。name变量就是枚举变量的字符串形式。

  • 要点

    使用的是enum关键字而不是class。

    多个枚举变量直接用逗号隔开。

    枚举变量最好大写,多个单词之间使用”_”隔开(比如:INT_SUM)。

    定义完所有的变量后,以分号结束,如果只有枚举变量,而没有自定义变量,分号可以省略(例如上面的代码就忽略了分号)。

    在其他类中使用enum变量的时候,只需要【类名.变量名】就可以了,和使用静态变量一样。

7.7.2 枚举的高级使用

就像我们前面的案例一样,你需要让每一个星期几对应到一个整数,比如星期天对应0。上面讲到了,枚举类在定义的时候会自动为每个变量添加一个顺序,从0开始。

假如你希望0代表星期天,1代表周一。。。并且你在定义枚举类的时候,顺序也是这个顺序,那你可以不用定义新的变量,就像这样:

public enum Weekday {
    SUN,MON,TUS,WED,THU,FRI,SAT
}

这个时候,星期天对应的ordinal值就是0,周一对应的就是1,满足你的要求。但是,如果你这么写,那就有问题了:

public enum Weekday {
    MON,TUS,WED,THU,FRI,SAT,SUN
}

我吧SUN放到了最后,但是我还是希0代表SUN,1代表MON怎么办呢?默认的ordinal是指望不上了,因为它只会傻傻的给第一个变量0,给第二个1。。。

所以,我们需要自己定义变量!

看代码:

public enum Weekday {
    MON(1),TUS(2),WED(3),THU(4),FRI(5),SAT(6),SUN(0);
    private int value;
    private Weekday(int value){
        this.value = value;
    }
}

我们对上面的代码做了一些改变:

首先,我们在每个枚举变量的后面加上了一个括号,里面是我们希望它代表的数字。

然后,我们定义了一个int变量,然后通过构造函数初始化这个变量。

你应该也清楚了,括号里的数字,其实就是我们定义的那个int变量。这句叫做自定义变量。

请注意:这里有三点需要注意

​ 1.一定要把枚举变量的定义放在第一行,并且以分号结尾。

​ 2.构造函数必须私有化。事实上,private是多余的,你完全没有必要写,因为它默认并强制是private,如果你要写,也只能写private,写public是不能通过编译的。

​ 3.自定义变量与默认的ordinal属性并不冲突,ordinal还是按照它的规则给每个枚举变量按顺序赋值。

既然能自定义一个变量,能不能自定义两个呢?

public enum Weekday {
    MON(1,"mon"),TUS(2,"tus"),WED(3,"wed"),THU(4,"thu"),
    FRI(5,"fri"),SAT(6,"sat"),SUN(0,"sun");
    private int value;
    private String label;
    private Weekday(int value,String label){
        this.value = value;
        this.label = label;
    }
}

7.7.3 枚举中的抽象类

如果我在枚举类中定义一个抽象方法会怎么样?

枚举类不能继承其他类,也不能被其他类继承。至于为什么,我们后面会说到。

你应该知道,有抽象方法的类必然是抽象类,抽象类就需要子类继承它然后实现它的抽象方法,但是呢,枚举类不能被继承。。你是不是有点乱?

我们先来看代码:

public enum TrafficLamp {
    RED(30) {
        @Override
        public TrafficLamp getNextLamp() {
            return GREEN;
        }
    }, GREEN(45) {
        @Override
        public TrafficLamp getNextLamp() {
            return YELLOW;
        }
    }, YELLOW(5) {
        @Override
        public TrafficLamp getNextLamp() {
            return RED;
        }
    };
    private int time;
    private TrafficLamp(int time) {
        this.time = time;
    }
    //一个抽象方法
    public abstract TrafficLamp getNextLamp();
}

你好像懂了点什么。但是你好像又不太懂。为什么一个变量的后边可以带一个代码块并且实现抽象方法呢?

别着急,带着这个疑问,我们来看一下枚举类的实现原理。

从最简单的看起:

public enum Weekday {
    SUN,MON,TUS,WED,THU,FRI,SAT
}

还是这段熟悉的代码,我们编译一下它,再反编译一下看看它到底是什么样子的:

JavaSE笔记 [全文字数7.1W]_第30张图片

你是不是觉得很熟悉?反编译出来的代码和我们一开始用静态变量自己写的那个类出奇的相似!而且,你看到了熟悉的values()方法和valueOf()方法。

仔细看,这个类继承了java.lang.Enum类!所以说,枚举类不能再继承其他类了,因为默认已经继承了Enum类。

并且,这个类是final的!所以它不能被继承!

回到我们刚才的那个疑问:

RED(30) {
    @Override
    public TrafficLamp getNextLamp() {
        return GREEN;
    }
}

为什么会有这么神奇的代码?现在你差不多懂了。因为RED本身就是一个TrafficLamp对象的引用。实际上,在初始化这个枚举类的时候,你可以理解为执行的是 TrafficLamp RED = new TrafficLamp(30) ,但是因为TrafficLamp里面有抽象方法,还记得匿名内部类么?

我们可以这样来创建一个TrafficLamp引用:

TrafficLamp RED = new TrafficLamp30{
    @Override
    public TrafficLamp getNextLamp() {
        return GREEN;
    }
};

而在枚举类中,我们只需要像上面那样写【RED(30){}】就可以了,因为java会自动的去帮我们完成这一系列操作。

如果你还是不太理解,那么你可以自己去反编译一下TrafficLamp这个类,看看jvm是怎么处理它的就明白了。

7.7.4 枚举的其他用法

switch语句中使用

enum Signal {
    GREEN, YELLOW, RED
}
public class TrafficLight {
    Signal color = Signal.RED;
    public void change() {
        switch (color) {
        case RED:
            color = Signal.GREEN;
            break;
        case YELLOW:
            color = Signal.RED;
            break;
        case GREEN:
            color = Signal.YELLOW;
            break;
        }
    }
}

实现接口

public interface Behaviour {
    void print();
    String getInfo();
}
public enum Color implements Behaviour {
    RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);
    // 成员变量
    private String name;
    private int index;
    // 构造方法
    private Color(String name, int index) {
        this.name = name;
        this.index = index;
    }
    // 接口方法
    @Override
    public String getInfo() {
        return this.name;
    }
    // 接口方法
    @Override
    public void print() {
        System.out.println(this.index + ":" + this.name);
    }
}

使用接口组织枚举

public interface Food {
    enum Coffee implements Food {
        BLACK_COFFEE, DECAF_COFFEE, LATTE, CAPPUCCINO
    }
    enum Dessert implements Food {
        FRUIT, CAKE, GELATO
    }
}

总结

  • 可以创建一个enum类,把它看做一个普通的类。除了它不能继承其他类了。(java是单继承,它已经继承了Enum),可以添加其他方法,覆盖它本身的方法
  • switch()参数可以使用enum
  • values()方法是编译器插入到enum定义中的static方法,所以,当你将enum实例向上转型为父类Enum是,values()就不可访问了。解决办法:在Class中有一个getEnumConstants()方法,所以即便Enum接口中没有values()方法,我们仍然可以通过Class对象取得所有的enum实例
  • 无法从enum继承子类,如果需要扩展enum中的元素,在一个接口的内部,创建实现该接口的枚举,以此将元素进行分组。达到将枚举元素进行分组。
  • enum允许程序员为eunm实例编写方法。所以可以为每个enum实例赋予各自不同的行为。

【7.7 枚举篇】转载自博主:clever_fan的 “ 重新认识java(十) ---- Enum(枚举类) ”

原文地址:http://blog.csdn.net/qq_31655965/article/details/55049192

第八章 异常

8.1 异常的分类

异常(Exception)位于 java.lang 包下,它是一种顶级接口,继承于 Throwable 类。

在程序在编译和运行阶段经常会产生异常或者错误,而认识异常之前,首先认识其父类 Throwable 类。

8.1.1 什么是Throwable

Throwable 类是Java语言中所有 错误 (errors)异常(exceptions) 的父类。只有继承于Throwable的类或其子类才能被抛出。还有一种方式是带有 @throw 注解的类也可以抛出。

Throwable类及其子类关系如下:

JavaSE笔记 [全文字数7.1W]_第31张图片

8.1.2 Exception及其子类

  • 程序产生异常异常主要分为:错误一般性异常(受控异常)运行期异常(非受控异常)

    • 错误:程序中无法处理的错误,表示运行应用程序中出现了严重的错误。此类错误一般表示代码运行时JVM出现问题。通常有Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如说当jvm耗完可用内存时,将出现OutOfMemoryError。此类错误发生时,JVM将终止线程。非代码性错误。因此,当此类错误发生时,应用不应该去处理此类错误。
    • 受控异常(编译期异常):Exception中除RuntimeException极其子类之外的异常。编译器会检查此类异常,如果程序中出现此类异常,比如说IOException,必须对该异常进行处理,要么使用try-catch捕获,要么使用throws语句抛出,否则编译不通过。
    • 非受控异常(运行期异常):RuntimeException类极其子类表示JVM在运行期间可能出现的错误。编译器不会检查此类异常,并且不要求处理异常,比如用空值对象的引用(NullPointerException)、数组下标越界(ArrayIndexOutBoundException)。此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。
  • 常见的异常:

    • RuntimeException

    JavaSE笔记 [全文字数7.1W]_第32张图片

    • UncheckedException

    JavaSE笔记 [全文字数7.1W]_第33张图片

8.2 异常的捕获

8.2.1 try、catch 和 finally

try {
    
}catch(OneException e) {
    
}catch(TwoException e) {
    
}finally {
    
}
  • try 中包含了可能产生异常的代码

  • try 后面是catch,catch 可以有一个或多个,catch 中是需要捕获的异常

  • 当try 中的代码出现异常时,出现异常下面的代码不会执行,马上会跳转到相应的catch 语句块中,如果没有异常不会跳转到catch 中

  • finally 表示,不管是出现异常,还是没有出现异常,finally 里的代码都执行,finally 和catch 可以分开使用,但 finally 必须和 try 一块使用,如下格式使用 finally 也是正确的

    try {
        
    }finally {
        
    }
    

异常捕获案例

public class ExceptionTest02 {
	public static void main(String[] args) {
        int i1 = 100;
        int i2 = 0;

        //try 里是出现异常的代码
        //不出现异常的代码最好不要放到try 作用
        try {
            //当出现被0 除异常,程序流程会执行到“catch(ArithmeticException ae)”语句
            //被0 除表达式以下的语句永远不会执行
            int i3 = i1/i2;

            //永远不会执行
            System.out.println(i3);

            //采用catch 可以拦截异常
            //ae 代表了一个ArithmeticException 类型的局部变量
            //采用ae 主要是来接收java 异常体系给我们new 的ArithmeticException对象
            //采用ae 可以拿到更详细的异常信息
        }catch(ArithmeticException ae) {
            System.out.println("被0 除了");
        }
	}
}

8.2.2 获取异常信息方法

  • 如何取得异常对象的具体信息,常用的方法主要有两种:

    取得异常描述信息:getMessage()

    取得异常的堆栈信息(比较适合于程序调试阶段):printStackTrace();

案例

public class ExceptionTest03 {
    public static void main(String[] args) {
        int i1 = 100;
        int i2 = 0;
        try {
            int i3 = i1/i2;
            System.out.println(i3);
            //ae 是一个引用,它指向了堆中的ArithmeticException
        }catch(ArithmeticException ae) {
            //通过getMessage 可以得到异常的描述信息
            System.out.println(ae.getMessage());
        }
    }
}
public class ExceptionTest04 {
    public static void main(String[] args) {
        method1();
    }
    private static void method1() {
        method2();
    }
    private static void method2() {
        int i1 = 100;
        int i2 = 0;
        try {
        	int i3 = i1/i2;
        	System.out.println(i3);
        //ae 是一个引用,它指向了堆中的ArithmeticException
        }catch(ArithmeticException ae) {
        	//通过printStackTrace 可以打印栈结构
        	ae.printStackTrace();
        }
    }
}

8.3 throw和throws

throw表示手动抛出异常throws表示声明抛出异常

1)throw用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结 束当前方法的执行。

使用的格式:

throw new 异常类名(参数);

示例:


public class DemoThrow {
	public static void main(String[] args) {
		int a =   DemoThrow.div(4,0);
		System.out.println(a);
	}
	public static int div(int a,int b){
		if(b==0){
			//抛出具体问题,编译时不检测
            throw new ArithmeticException("异常信息:除数不能为0");
        }
		return a/b;
	}
}

JavaSE笔记 [全文字数7.1W]_第34张图片

2)throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常

使用格式:

修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2 ... { }

示例:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
public class DemoThrows {
	public static void main(String[] args) throws FileNotFoundException{
        readFile();
    }
    
    public static  void readFile() throws FileNotFoundException {
        InputStream is = new FileInputStream("E:/iodemo/ch01.txt");
    }
}

JavaSE笔记 [全文字数7.1W]_第35张图片

8.4 自定义异常

除了JDK定义好的异常类外,在开发过程中根据业务的异常情况自定义异常类。

package com.example.springbootmybatis;

/**
 * 自定义异常类
 * 用户不存在异常信息类
 */
public class UserNotExistsException extends RuntimeException{

    public UserNotExistsException() {
        super();
    }
    public UserNotExistsException(String message) {
        super(message);
    }
}

第九章 集合

9.1 集合的结构

9.1.1 集合概述

集合实际上就是一个容器,数组也是集合。

集合不能直接存储基本数据类型,也不能直接存储java对象,集合当中存储的都是java对象的内存地址。(或者说集合中存储的是引用。)

在java中每一个不同的集合,底层会对应不同的数据结构。往不同的集合中存储元素,等于将数据放到了不同的数据结构当中。什么是数据结构?数据存储的结构就是数据结构。不同的数据结构,数据存储方式不同。例如:数组、二叉树、链表、哈希表…

  • 集合分为两大类:

    一类是单个方式存储元素:

    ​ 单个方式存储元素,这一类集合中超级父接口:java.util.Collection

    一类是以键值对儿的方式存储元素:

    ​ 以键值对的方式存储元素,这一类集合中超级父接口:java.util.Map

  • Collection 下的 ListSetMap 共同组成 Java 中最常用的三种集合顶层接口

  • ListSetMap 接口树状图:

Collection 接口的接口 对象的集合(单列集合)
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
│—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
│—————-└ Vector 接口实现类 数组, 同步, 线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,不可重复,并做内部排序
├—————-└HashSet 使用hash表(数组)存储元素
│————————└ LinkedHashSet 链表维护元素的插入次序
└ —————-TreeSet 底层实现为二叉树,元素排好序

Map 接口 键值对的集合 (双列集合)
├———Hashtable 接口实现类, 同步, 线程安全
├———HashMap 接口实现类 ,没有同步, 线程不安全-
│—————–├ LinkedHashMap 双向链表和哈希表实现
│—————–└ WeakHashMap
├ ——–TreeMap 红黑树对所有的key进行排序
└———IdentifyHashMap

9.1.2 集合继承关系

Collection继承结构图

JavaSE笔记 [全文字数7.1W]_第36张图片

Map继承结构图

JavaSE笔记 [全文字数7.1W]_第37张图片

9.1.3 集合和数组的区别

JavaSE笔记 [全文字数7.1W]_第38张图片

9.2 Iterable和Collection

9.2.1 Iterable接口

Iterable接口是Collection接口的父接口,在集合中属爷爷辈的顶层 接口

主要有两种功能:

① 实现此接口允许对象成为 for-each 的循环目标

② 提供子接口——迭代接口 Iterator

**Iterator迭代器**作用:遍历集合中的数据

​ 主要方法:

boolean hasNext();//如果仍有元素可以迭代,则返回true。
E next();//返回迭代的下一个元素。

迭代器迭代元素的过程中不能使用集合对象的remove方法删除元素,要使用迭代器Iterator的remove方法来删除元素,防止出现异常:ConcurrentModificationException

迭代原理图

JavaSE笔记 [全文字数7.1W]_第39张图片

9.2.2 Collection接口

Collection是单列集合的顶层父类接口,它主要用来定义集合的约定。

Collection 分为 List 和 Set 两大分支。List 必须按照插入的顺序保存元素,而 Set 不能有重复的元素。

Collection 的主要方法

JavaSE笔记 [全文字数7.1W]_第40张图片

9.3 List

List 接口也是一个顶层接口,它集成了Collection接口,同时也是ArrayList、LinkedList等集合元素的父类。

  • List 集合的主要特点:有序可重复有索引

  • List 接口的实现:

    • ArrayList:查询数据比较快,添加和删除数据比较慢(基于可变数组)
    • LinkedList:查询数据比较慢,添加和删除数据比较快(基于链表数据结构)
    • Vector:Vector 已经不建议使用,Vector 中的方法都是同步的,效率慢,已经被ArrayList
      取代
    • Stack 是继承Vector 实现了一个栈,栈结构是后进先出,目前已经被LinkedList 取代
  • 常用方法:

    //向指定下标添加元素,后面的元素后移一位(效率低)
    void add(int index, Object element);
    
    //删除指定下标位置的元素
    Object remove(int indext);
    
    //修改指定位置的元素
    Object set(int index, Object element);
        
    //根据下标获取元素,可通过下标遍历,List集合特有
    Object get(int index);
    
    //获取指定对象第一次出现处的索引
    int indexOf(Object o);
    
    //获取指定对象最后一次出现处的索引
    int lastIndextOf(Object o);
    
    //列表迭代器
    ListIterator listIterator();
        
    //截取集合
    List subList(int fromIndex, int toIntex);
    

9.3.1 ArrayList(数组)

​ ArrayList 是最常用的List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。

特点:底层是数组结构,查询数据快增删数据慢

细节

  • ArrayList集合初始化容量10

  • 扩容为原容量1.5倍。

  • 底层是Object[]数组。

  • ArrayList 扩容相当于数组扩容,存在拷贝效率低的问题。

  • 但是向尾部增删数据效率还是高的。

  • ArrayList 不是线程安全的容器,如果多个线程同时修改了ArrayList的结构会导致线程安全问题,可用线程安全的List作为替代,例如:Collections.synchronizedList

    List list = Collections.synchronizedList(new ArrayList(...));
    

9.3.2 LinkedList(双向链表)

LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较
慢。另外,他还提供了List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆
栈、队列和双向队列使用。

特点:底层是双向链表结构,查询数据慢增删数据快

链表结构简介

​ 1)单向链表

JavaSE笔记 [全文字数7.1W]_第41张图片

​ 2)双向链表

JavaSE笔记 [全文字数7.1W]_第42张图片

LinkedList内存图

JavaSE笔记 [全文字数7.1W]_第43张图片

细节

  • LinkedList 也不是线程安全的,如果要避免线程安全问题,链表必须外部加锁,或者使用:

    List list = Collections.synchronizedList(new LinkedList(...));
    
  • LinkedList 在内存中的地址是不连续的。

  • LinkedList 查找元素只能从头结点或尾结点开始遍历查找。

  • LinkedList 随机增删元素只改变前后两个节点的内容,而不像 ArrayList 那样导致大量元素位移。

特有方法

//插入元素
void addFirst(E e);
void addLast(E e);

//获取元素
E getFirst();
E getLast();

//删除元素
E removeFirst();
E removeLast();

9.4 Set

Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重
复。对象的相等性本质是对象hashCode 值(java 是依据对象的内存地址计算出的此序号)判断
的,如果想要让两个不同的对象视为相等的,就必须覆盖Object 的hashCode 方法和equals 方
法。

  • Set 集合的主要特点:无序不可重复没有索引

  • Set 接口的实现:

    • HashSet:基于哈希表,元素无序且唯一
    • TreeSet:基于二叉树,元素有序且唯一
    • LinkHashSet:基于链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性

9.4.1 哈希表、二叉树

哈希表

​ 哈希表是一种数据结构,哈希表能够提供快速存取操作。哈希表是一个数组和单向链表的结合体,也就是哈希表等同于,一个一维数组,数组中每个元素是一个单向链表,单向链表中的Node节点有四个属性(hash哈希值,key,value,next下一节点地址)

​ 正常的数组,如果需要查询某个值,需要对数组进行遍历,只是一种线性查找,查找的速度比较慢。如果数组中的元素值和下标能够存在明确的对应关系,那么通过数组元素的值就可以换算出数据元素的下标,通过下标就可以快数定位数组元素,这样的数组就是哈希表。

​ 哈希表图解:

JavaSE笔记 [全文字数7.1W]_第44张图片

二叉树

​ 二叉树图解:

JavaSE笔记 [全文字数7.1W]_第45张图片

9.4.2 HashSet(Hash 表)

哈希表边存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序(和List 显然不
同)而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的
hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较
equals 方法如果equls 结果为true ,HashSet 就视为同一个元素。如果equals 为false 就不是
同一个元素。

特点:HashSet底层实际是使用HashMap存储数据,HashSet实际上是HashMap的key部分,而判断是否同一条数据是hashcode和equals() 共同作用的结果。

覆盖hashCode()方法的原则

  1. 一定要让那些我们认为相同的对象返回相同的hashCode值。
  2. 尽量让那些我们认为不同的对象返回不同的hashCode值,否则,就会增加冲突的概率。
  3. 尽量的让hashCode值散列开(两值用异或运算可使结果的范围更广)

9.4.3 TreeSet(二叉树)

  1. TreeSet()是使用二叉树的原理对新add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置,不允许null值。
  2. Integer 和String 对象都可以进行默认的TreeSet 排序,而自定义类的对象是不可以的,自己定义的类必须实现Comparable 接口,并且覆写相应的compareTo()函数,才可以正常使用。
  3. 在覆写compare()函数时,要返回相应的值才能使TreeSet 按照一定的规则来排序。
  4. 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。

9.4.4 LinkHashSet(HashSet+LinkedHashMap)

对于LinkedHashSet 而言,它继承与HashSet 、又基于LinkedHashMap 来实现的。LinkedHashSet 底层使用LinkedHashMap 来保存所有元素,它继承与HashSet,其所有的方法操作上又与HashSet 相同,因此LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个LinkedHashMap 来实现,在相关操作上与父类HashSet 的操作相同,直接调用父类HashSet 的方法即可。

9.4.5 小结

​ Set具有与Collection完全一样的接口,因此没有任何额外的功能,不像前面有两个不同的List。实际上Set就是Collection,只 是行为不同。(这是继承与多态思想的典型应用:表现不同的行为。)Set不保存重复的元素。

​ Set 存入Set的每个元素都必须是唯一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序。

9.5 Map

​ Map 中可以放置键值对,也就是每一个元素都包含键对象和值对象,Map 实现较常用的为HashMap,HashMap 对键对象的存取和HashSet 一样,仍然采用的是哈希算法,所以如果使用自定类作为Map 的键对象,必须复写equals 和hashCode 方法。

​ Map 没有继承 Collection 接口, Map 提供 key 到 value 的映射,你可以通过“键”查找“值”。一个 Map 中不能包含相同的 key ,每个 key 只能映射一个 value 。 Map 接口提供 3 种集合的视图, Map 的内容可以被当作一组 key 集合,一组 value 集合,或者一组 key-value 映射。

常用子类:HashMap、TreeMap

常用方法

//清除所有映射
void clear();

//查询Map是否包含指定key
boolean containsKey(Object key);

//将Map转换为Set集合,集合的每个元素都是Map.Entry类型
Set<Map.Entry<K,V>> entrySet();

//返回指定key对应的value,如果没有该key则返回null
V get(Object key);

//查询Map是否为空,如果空则返回true
boolean isEmpty();

//返回该Map中所有key组成的Set集合
Set<K> keySet();

//添加一个键值对,如果已有一个相同的Key则覆盖旧的键值对
V put(K key, V value);

//将指定的Map的键值对复制到Map中
void putAll(Map<? extends K,? extends V> m);

//删除指定Key所对应的键值对,返回关联的value,如果key不存在则返回null
V remove(Object key);

//返回Map里键值对的个数
int size();

//返回Map中所有的value组成的集合
Collection<V> values();

Map转为Set集合,entrySet()方法的图示:

JavaSE笔记 [全文字数7.1W]_第46张图片

9.5.1 HashMap

​ HashMap 根据键的hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。HashMap 最多只允许一条记录的键为null,允许多条记录的值为null。HashMap 非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用Collections 的synchronizedMap 方法使HashMap 具有线程安全的能力,或者使用ConcurrentHashMap。我们用下面这张图来介绍HashMap 的结构。

JavaSE笔记 [全文字数7.1W]_第47张图片

​ 大方向上,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个绿色
的实体是嵌套类Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的next。

  1. capacity:当前数组容量,始终保持2^n,可以扩容,扩容后数组大小为当前的2 倍。
  2. loadFactor:负载因子,默认为0.75。
  3. threshold:扩容的阈值,等于capacity * loadFactor

​ Java8 对HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由数组+链表+红黑树组成。

​ 根据Java7 HashMap 的介绍,我们知道,查找的时候,根据hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为O(n)。为了降低这部分的开销,在Java8 中,当链表中的元素超过了8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为O(logN)。

JavaSE笔记 [全文字数7.1W]_第48张图片

9.5.2 TreeMap

JavaSE笔记 [全文字数7.1W]_第49张图片

如果是自定义类型,需要实现 Comparable 接口,并重写 compareTo() 方法,才能实现排序。

9.6 Collections工具类

Collections工具类位于 java.util 包下,这个类只包含操作或返回集合的静态方法。

常用方法

//线程安全类
static <T> Collection<T> synchronizedCollection(Collection<T> c);
static <T> List<T> synchronizedList(List<T> list);
static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
static <T> Set<T> synchronizedSet(Set<T> s);
static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);
static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s);

//算法类
static <T extends Comparable<? super T>> void sort(List<T> list);//排序
static <T> void sort(List<T> list, Comparator<? super T> c);//指定比较器排序
static void swap(List<?> list, int i, int j);//交换元素

9.7 泛型

在 JDK 5.0 中,提出了一个新特性,泛型。泛型是一种编译时类型确认机制。它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现ClassCastException。

作用:泛型能更早的发现错误,如类型转换错误,通常在运行期才会发现,如果使用泛型,那么在编译期将会发现,通常错误发现的越早,越容易调试,越容易减少成本。

9.7.1 泛型的使用

1)用泛型表示类

//此处 E 表示任意类型
public class GenericDemo<T>{
	//value这个成员变量的类型为T,T的类型由外部指定
    private T value;
	
    public GenericDemo(T value) {
		this.value = value;
	}
    
    //get方法返回的T类型由外部指定
    public T getValue(){
        return value;
    }

    public void setValue(T value){
        this.value = value;
    }

2)用泛型表示接口

public interface Generator<T> {
	public T next();
}

一般泛型接口常用于生成器(generator)中,相当于对象工厂,专门用来创建对象。

3)泛型表示方法

public class GenericMethods {
    public <T> void f(T x){
        System.out.println(x.getClass().getName());
    }
}

4)泛型通配符

public static void main(String[] args) {
    List<String> name = new ArrayList<String>();
    List<Integer> age = new ArrayList<Integer>();
    List<Number> number = new ArrayList<Number>();
    name.add("cxuan");
    age.add(18);
    number.add(314);
    generic(name);
    generic(age);
    generic(number);
}

public static void generic(List<?> data) {
	System.out.println("Test cxuan :" + data.get(0));
}

上界通配符: 表示接受该类型及其子类

下界通配符: 表示接受该类及其父类

无界通配符: 表示任意类型

5)类型擦除

Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的Java 字节代码中是不包含泛型中的类型信息的。==使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。==如在代码中定义的ListList等类型,在编译之后都会变成List。JVM 看到的只是List,而由泛型附加的类型信息对JVM 来说是不可见的。类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。

9.7.2 JDK8 泛型新特性

JDK8.0之后引入了一种 “泛型自动推断机制” ,也叫钻石表达式。

public class GenericTest(){
    public static void main(String[] args){
        //这里的ArrayList类型会自动推断;<> JDK8.0之后才允许
        List<Animal> list = new ArrayList<>();
    }
}

第十章 IO流

​ 文件通常是由一连串的字节或字符构成,组成文件的字节序列称为字节流,组成文件的字符序列称为字符流。Java 中根据流的方向可以分为输入流和输出流。输入流是将文件或其它输入设备的数据加载到内存的过程;输出流恰恰相反,是将内存中的数据保存到文件或其他输出设备。

​ 文件是由字符或字节构成,那么将文件加载到内存或再将文件输出到文件,需要有输入和输出流的支持,那么在Java 语言中又把输入和输出流分为了两个,字节输入和输出流,字符输入和输出流

10.1 IO流概述

以字节流字符流作为区分

JavaSE笔记 [全文字数7.1W]_第50张图片

以操作对象作为区分

JavaSE笔记 [全文字数7.1W]_第51张图片

IO类有很多,但是最基本的是四个抽象类 InputStreamOutputStreamReaderWriter

10.1.1 InputStream 字节输入流

InputStream 是字节输入流,抽象类,所有继承了InputStream 的类都是字节输入流。

主要方法

//返回可读的字节数量
int available();

//从输入流读取下一个数据字节
abstract int read();
//读取下一个数据字节,存入数组b中
int read(byte[] b);
//从第off位置开始,读取len长度字节的数据,存放到b数组
int read(byte[] b, int off, int len);

//关闭流,释放资源
void close();

//跳过n个字节的数据
long skip(long n);

10.1.2 OutputStream 字节输出流

OutputStream 是字节输出流,抽象类,该类所有方法都返回一个 void ,在出错时抛出一个 IOException 。

主要方法

//关闭此输出流并释放与此流有关的所有系统资源。
void close();

//刷新此输出流并强制写出所有缓冲的输出字节。
void flush();

//将指定的字节写入此输出流。
abstract void write(int b);

//将b.length 个字节从指定的字节数组写入此输出流。
void write(byte[] b);

//将指定字节数组中从偏移量off 开始的len 个字节写入此输出流。
void write(byte[] b, int off, int len);

10.1.3 Reader 字符输入流

Reader 是字符输入流,抽象类,在出错时抛出一个 IOException 。

主要方法

//关闭该流。
abstract void close();

//读取单个字符。
int read();
//将字符读入数组。
int read(char[] cbuf);
//将字符读入数组的某一部分
abstract int read(char[] cbuf, int off, int len);

//跳过n个字符
long skip(long n)

10.1.4 Writer 字符输出流

Writer 是字符输出流,抽象类,该类所有方法都返回一个 void ,在出错时抛出一个 IOException 。

主要方法

//将指定字符追加到此writer。
Writer append(char c);

//关闭此流,但要先刷新它。
abstract void close();

//刷新此流。
abstract void flush();

//写入字符数组。
void write(char[] cbuf);
//写入字符数组的某一部分。
abstract void write(char[] cbuf, int off, int len);

//写入单个字符。
void write(int c);
//写入字符串。
void write(String str);
//写入字符串的某一部分。
void write(String str, int off, int len);

10.2 文件流

10.2.1 FileInputStream(文件字节输入流)

FileInputStream:字节文件输入流,从文件系统中的某个文件中获得输入字节,用于读取诸如图像数据之类的原始字节流。

案例演示:

import java.io.*;
public class FileInputStreamTest01 {
    public static void main(String[] args) {
        InputStream is = null;
        try {
            is = new FileInputStream("c:\\test.txt");
            int b = 0;
            while ((b = is.read()) != -1) {
                //直接打印
                //System.out.print(b);
                //输出字符
                System.out.print((char)b);
            }
        }catch(FileNotFoundException e) {
            e.printStackTrace();
        }catch(IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if (is != null) {
                is.close();
                }
            }catch(IOException e) {}
        }
    }
}

注:文件可以正确的读取,但是我们的汉字乱码了,原因在于使用了字节输入流,它是一个字节一个字节读取的,而汉字是两个字节,所以读出一个字节就打印,那么汉字是不完整的,所以就乱码了

10.2.2 FileOutputStream(文件字节输出流)

FileOutputStream:字节文件输出流是用于将数据写入到File,从程序中写入到其他位置。

案例演示:

import java.io.*;
public class FileOutputStreamTest01 {
    public static void main(String[] args) {
        InputStream is = null;
        OutputStream os = null;
        try {
            is = new FileInputStream("c:\\test.txt");
            os = new FileOutputStream("d:\\test.txt.bak");
            int b = 0;
            while ((b = is.read()) != -1) {
            	os.write(b);
            }
            System.out.println("文件复制完毕!");
        }catch(FileNotFoundException e) {
        	e.printStackTrace();
        }catch(IOException e) {
        	e.printStackTrace();
        }finally {
            try {
                if (is != null) {
                	is.close();
                }
                if (os != null) {
                	os.close();
            	}
            }catch(IOException e) {}
        }
    }
}

10.2.3 FileReader(文件字符输入流)

FileReader 是一字符为单位读取文件,也就是一次读取两个字节。

案例演示:

import java.io.*;
public class FileReaderTest01 {
    public static void main(String[] args) {
        Reader r = null;
        try {
            r = new FileReader("c:\\test.txt");
            int b = 0;
            while ((b = r.read()) != -1) {
                //输出字符
                System.out.print((char)b);
            }
        }catch(FileNotFoundException e) {
        	e.printStackTrace();
        }catch(IOException e) {
        	e.printStackTrace();
        }finally {
            try {
                if (r != null) {
                	r.close();
            	}
            }catch(IOException e) {}
        }
    }
}

注:因为采用了字符输入流读取文本文件,所以汉字就不乱吗了,因为一次读取两个字节(即一个字符)

10.2.3 FileReader(文件字符输入流)

案例演示:

import java.io.*;
public class FileWriterTest01 {
    public static void main(String[] args) {
        Writer w = null;
        try {
            //以下方式会将文件的内容进行覆盖
            //w = new FileWriter("c:\\test.txt");
            //w = new FileWriter("c:\\test.txt", false);
            //以下为true 表示,在文件后面追加
            w = new FileWriter("c:\\test.txt", true);
            w.write("你好你好!!!!");
            //换行
            w.write("\n");
        }catch(FileNotFoundException e) {
        	e.printStackTrace();
        }catch(IOException e) {
        	e.printStackTrace();
        }finally {
            try {
                if (w != null){
                	w.close();
                }
            }catch(IOException e) {}
        }
    }
}

10.3 缓冲流

​ 缓冲流主要是为了提高效率而存在的,减少物理读取次数,缓冲流主要有:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter,并且BufferedReader 提供了实用方法readLine(),可以直接读取一行,BufferWriter 提供了newLine()可以写换行符。

10.3.1 字节缓冲流

BufferedInputStream:字节缓冲输入流,提高了读取效率。

BufferedOutputStream:字节缓冲输出流,提高了写出效率。

案例演示:

import java.io.*;
public class BufferedStreamTest01 {
    public static void main(String[] args) {
        InputStream is = null;
        OutputStream os = null;
        try {
            is = new BufferedInputStream(new FileInputStream("c:\\test.txt"));
            os = new BufferedOutputStream(new FileOutputStream("d:\\test.txt.bak"));
            int b = 0;
            while ((b = is.read()) != -1) {
            	os.write(b);
            }
            //手动调用flush,将缓冲区中的内容写入到磁盘
            //也可以不用手动调用,缓存区满了自动回清楚了
            //而当输出流关闭的时候也会先调用flush
            os.flush();
            System.out.println("文件复制完毕!");
        }catch(FileNotFoundException e) {
        	e.printStackTrace();
        }catch(IOException e) {
        	e.printStackTrace();
        }finally {
            try {
                if (is != null) {
                	is.close();
                }
                if (os != null) {
                    //在close 前会先调用flush
                    os.close();
                }
            }catch(IOException e) {}
        }
    }
}

10.3.2 字符缓冲流

案例演示:

import java.io.*;
public class BufferedReaderTest01 {
    public static void main(String[] args) {
        BufferedReader r = null;
        BufferedWriter w = null;
        try {
            r = new BufferedReader(new FileReader("c:\\test.txt"));
            w = new BufferedWriter(new FileWriter("d:\\test.txt.bak"));
            String s = null;
            while ((s = r.readLine()) != null) {
                w.write(s);
                //w.write("\n");
                //可以采用如下方法换行
                w.newLine();
            }
            System.out.println("文件复制完毕!");
        }catch(FileNotFoundException e) {
        	e.printStackTrace();
        }catch(IOException e) {
        	e.printStackTrace();
        }finally {
            try {
                if (r != null) {
                	r.close();
                }
                if (w != null) {
                    //在close 前会先调用flush
                    w.close();
                }
            }catch(IOException e) {}
        }
    }
}

10.4 转换流

  • 转换流主要有两个InputStreamReader 和OutputStreamWriter
    • InputStreamReader 主要是将字节流输入流转换成字符输入流
    • OutputStreamWriter 主要是将字节流输出流转换成字符输出流

10.4.1 InputStreamReader

案例演示:

import java.io.*;
public class InputStreamReaderTest01 {
    public static void main(String[] args) {
        BufferedReader br = null;
        try {
            br = new BufferedReader(
                    new InputStreamReader(
                    new FileInputStream("c:\\test.txt")));
            String s = null;
            while ((s = br.readLine()) != null) {
            	System.out.println(s);
            }
        }catch(FileNotFoundException e) {
        	e.printStackTrace();
        }catch(IOException e) {
        	e.printStackTrace();
        }finally {
            try {
                if (br != null) {
                	br.close();
                }
            }catch(IOException e) {}
        }
    }
}

10.4.2 OutputStreamWriter

import java.io.*;
public class OutputStreamWriterTest01 {
    public static void main(String[] args) {
        BufferedWriter bw = null;
        try {
            bw = new BufferedWriter(
                    new OutputStreamWriter(
                    new FileOutputStream("c:\\603.txt")));
            bw.write("asdfsdfdsf");
            bw.newLine();
            bw.write("风光风光风光好");
        }catch(FileNotFoundException e) {
        	e.printStackTrace();
        }catch(IOException e) {
        	e.printStackTrace();
        }finally {
            try {
                if (bw != null) {
                	bw.close();
                }
            }catch(IOException e) {}
        }
    }
}

10.5 打印流

打印流主要包含两个:PrintStream 和PrintWriter,分别对应字节流和字符流

10.5.1 完成屏幕打印的重定向

System.out 其实对应的就是PrintStream,默认输出到控制台,我们可以重定向它的输出,可以定向到文件,也就是执行System.out.println(“hello”)不输出到屏幕,而输出到文件。

案例演示:

import java.io.*;
public class PrintStreamTest01 {
    public static void main(String[] args) {
        OutputStream os = null;
        try {
            os = new FileOutputStream("c:/console.txt");
            System.setOut(new PrintStream(os));
            System.out.println("asdfkjfd;lldffdfdrerere");
        }catch(FileNotFoundException e) {
        	e.printStackTrace();
        }catch(IOException e) {
        	e.printStackTrace();
        }finally {
            try {
                if (os != null) {
                	os.close();
                }
            }catch(IOException e) {}
        }
    }
}

10.5.2 接受屏幕输入

案例演示:

import java.io.*;
public class PrintStreamTest02 {
    public static void main(String[] args) {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new InputStreamReader(System.in));
            String s = null;
            while ((s=br.readLine()) != null) {
                System.out.println(s);
                //为q 退出循环
                if ("q".equals(s)) {
                	break;
                }
            }
        }catch(FileNotFoundException e) {
        	e.printStackTrace();
        }catch(IOException e) {
        	e.printStackTrace();
        }finally {
            try {
                if (br != null) {
                	br.close();
                }
            }catch(IOException e) {}
        }
    }
}

10.6 对象流

​ 对象流可以将Java 对象转换成二进制写入磁盘,这个过程通常叫做序列化,并且还可以从磁盘读出完整的Java 对象,而这个过程叫做反序列化。

​ 对象流主要包括:ObjectInputStreamObjectOutputStream

10.6.1 序列化

将对象转换成字节序列的过程,就是对象序列化过程。

对象序列化的主要用途

​ 1.把对象转换成字节序列,保存到硬盘当中,持久化存储,通常保存为文件。

​ 2.在网络上传递的是对象的字节序列。

对象序列化的步骤

​ 1.创建对象输出流,在构造方法当中可以包含其他输出节点流,如文件输出流。

​ 2.把对象通过 writeObject 的方式写入。

注意事项

​ 1.只有实现了 Serializable 接口的类的对象才能够被序列化。

​ 2.先序列化然后在反序列化,而且反序列化的顺序必须和序列化的顺序保持一致。

​ 3.对象当中并不是所有的属性都能够被序列化。

​ 4.transient :当类中有属性不想被序列化,那么就使用这个修饰符修饰。

案例演示:

import java.io.*;
public class ObjectStreamTest02 {
    public static void main(String[] args) {
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("c:/Person.dat"));
            Person person = new Person();
            person.name = "张三";
            oos.writeObject(person);
        }catch(FileNotFoundException e) {
        	e.printStackTrace();
        }catch(IOException e) {
        	e.printStackTrace();
        }finally {
            try {
                if (oos != null) {
                	oos.close();
                }
            }catch(IOException e) {}
        }
    }
}

//实现序列化接口
class Person implements Serializable{
	String name;
}

10.6.2 反序列化

对象反序列化的步骤

​ 1.创建对象输入流,在构造方法当中可以包含其他的输入节点流,如文件输入流

​ 2.通过readObject()方法读取对象。

案例演示:

import java.io.*;
public class ObjectStreamTest03 {
    public static void main(String[] args) {
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream("c:/Person.dat"));
            //反序列化
            Person person = (Person)ois.readObject();
            System.out.println(person.name);
        }catch(ClassNotFoundException e) {
        	e.printStackTrace();
        }catch(FileNotFoundException e) {
        	e.printStackTrace();
        }catch(IOException e) {
        	e.printStackTrace();
        }finally {
            try {
                if (ois != null) {
                	ois.close();
                }
            }catch(IOException e) {}
        }
    }
}

//实现序列化接口
class Person implements Serializable{
	String name;
}

10.6.3 serialVersionUID

  • serialVersionUID :序列化版本id

  • 作用:从字面角度看,就是序列化版本号。凡是实现了Serializable接口的类,都会有一个默认的静态的序列化标识。

    • 1.类在不同的版本之间,可以解决序列化兼容问题,如果之前版本当中在文件中保存对象,那么版本升级后,如果序列化id一致,我们可以认为文件中的对象依然是此类的对象。

    • 2.如果类在不同的版本之间不希望兼容,但是还希望类的对象能够序列,那么就在不同版本中使用不同的序列化id。

10.7 File 类

  • File类的由来:File类的出现弥补了IO流的不足,IO只能够操作数据,但是不能够对文件的信息做操作,操作文件必须使用File类。

  • 功能:

    • 可以将文件或者文件夹在程序当中分装成对象。

    • 方便对于文件或者文件夹当中的属性信息进行操作。

    • File类通常通过构造函数作为参数传递到流的对象当中。

  • 属性(静态常量):

    • separator :与系统有关的默认名称分隔符,为了方便,它被表示为一个字符串。此字符串只包含一个字符,即 separatorChar。
    • separatorChar :与系统有关的默认名称分隔符。此字段被初始化为包含系统属性 file.separator 值的第一个字符。在 UNIX 系统上,此字段的值为 ‘/’;在 Microsoft Windows 系统上,它为 ‘\’。
    • pathSeparator :与系统有关的路径分隔符,为了方便,它被表示为一个字符串。此字符串只包含一个字符,即 pathSeparatorChar。
    • pathSeparatorChar :与系统有关的路径分隔符。此字段被初始为包含系统属性 path.separator 值的第一个字符。此字符用于分隔以路径列表 形式给定的文件序列中的文件名。在 UNIX 系统上,此字段为 ‘:’;在 Microsoft Windows 系统上,它为 ‘;’。

10.7.1 File 类常用方法

构造方法:

//通过将给定路径名字符串转换为抽象路径名来创建一个新 File 实例。
File(String pathname);

//根据 parent 抽象路径名和 child 路径名字符串创建一个新 File 实例。
File(File parent,String child);

//根据 parent 路径名字符串和 child 路径名字符串创建一个新 File 实例。
File(String parent,String child);

创建文件相关函数:

//当且仅当不存在具有此抽象路径名指定名称的文件时,不可分地创建一个新的空文件。
boolean createNewFile();

//在默认临时文件目录当中创建一个空文件,程序运行结束后就不存在了。
static File createTemFile();

//创建目录,如果你写的目录的父目录不存在。他会帮你创建好。
boolean mkdirs();

删除文件相关函数:

//删除空目录或文件(ps只能是空目录)
boolean delete();

//在虚拟机终止时删除文件。
void deleteOnExit();

判断:

//判断文件或者文件夹是否存在。
boolean exists();

//判断文件是否可执行,和操作系统相关。
boolean canExecute();

//判断文件是否可读
boolean canRead();

//判断文件是否可写
boolean canWrite();

//测试此抽象路径名与给定对象是否相等。
boolean equals(Object obj);

//测试此抽象路径名是否为绝对路径名。
boolean isAbsolute();

//判断file对象是否表示文件夹。
boolean isDirectory();

//判断file对象是否表示文件
boolean isFile();

//判断file对象是否是隐藏文件
boolean isHidden();

获取file对象属性信息的方法:

//返回此抽象路径名的绝对路径名形式。
getAbsoluteFile();

//返回此抽象路径名的绝对路径名字符串。
getAbsolutePath();

//返回此抽象路径名的规范形式。
getCanonicalFile();

//回此抽象路径名的规范路径名字符串。
getCanonicalPath();

//将此抽象路径名转换为一个路径名字符串。
getPath();

//返回由此抽象路径名表示的文件或目录的名称。
getName();

//返回此抽象路径名父目录的路径名字符串;如果此路径名没有指定父目录,则返回 null。
getParent();

//返回此抽象路径名父目录的抽象路径名;如果此路径名没有指定父目录,则返回 null。
getParentFile();

//返回指定路径的全部空间的字节数
getTotalSpace();

//返回此抽象路径名指定的分区中未分配的字节数。
getFreeSpace();

//返回此抽象路径名指定的分区上可用于此虚拟机的字节数。
getUsableSpace();

//重新命名此抽象路径名表示的文件。剪切
renameTo(File dest);

设置文件信息的方法:

//设置文件可执行的方法
setExecutable(boolean executable);

//设置此抽象路径名指定的文件或目录的最后一次修改时间。
setLastModified(long time);

//设置文件是否可读
setReadable(boolean readable);

//设置文件是否只读
setReadOnly();

//设置文件是否可写
setWritable(boolean writable);

获取文件的常规信息的方法:

//获取文件最后一次被修改的时间
lastModified();

//返回由此抽象路径名表示的文件的长度。
length();

操作文件夹的相关方法:

//把文件夹当中包含的目录和文件都存放到字符串数组当中。
list();

//列举文件夹当中包含的目录和文件,存放到File数组当中。
listFiles();

//列出可用的文件系统根。
listRoots();

第十一章 多线程

11.1 多线程的基本概念

线程指进程中的一个执行场景,也就是执行流程,那么进程和线程有什么区别呢?

  • 每个进程是一个应用程序,都有独立的内存空间。
  • 同一个进程中的线程共享其进程中的内存和资源。
  • 共享的内存是堆内存和方法区内存,栈内存不共享,每个线程有自己的。

11.1.1 进程简介

什么是进程

​ 一个进程就是一个应用程序。在操作系统中每启动一个应用程序就会相应的启动一个进程。例如:Word 进程,QQ 进程,JVM 启动对应一个进程。

多进程的作用

​ 最初的计算机是“单进程的”,计算机只能运行一个应用程序,例如第一台计算机只有DOS 窗口。现代的计算机可以满足我们一边听音乐,一边玩游戏。现代的计算给我们人类感觉:多件事情一起运行。感觉是并行的(错觉)。

​ 对于单核的计算机来讲,在某一个时间点上只能做一件事情,但是由于计算机的处理速度很高,多个进程之间完成频繁的切换执行,这个切换速度使人类产生了错觉,人类的错觉是:多个进程在同时运行。

​ 计算机引入多进程的作用:提高CPU 的使用率。

重点:进程和进程之间的内存独立。

11.1.2 线程简介

什么是线程

​ 线程是进程的一个执行场景。一个进程可以启动多个线程。

多线程的作用

​ 提高进程的使用率。

重点:线程和线程之间栈内存独立,堆内存和方法区内存共享。一个线程一个栈。

11.1.3 并行与并发

并发:多个线程交替执行,抢占cpu的时间片,但是速度很快,在宏观角度看来就像是多个线程同时执行。

并行:多个线程在不同的cpu中同时执行。

并发与并行的区别

​ 并发严格的说不是同时执行多个线程,只是线程交替执行且速度很快,相当于同时执行。

​ 而并行是同时执行多个线程,也就是多个cpu核心同时执行多个线程。

在实际开发中,我们不需要关心是否是并发还是并行,因为cpu会帮我们处理多线程,开发中可以认为多线程就是同时执行多个线程。

11.1.4 java程序的执行流程

​ java 命令执行会启动JVM,JVM 的启动表示启动一个应用程序,表示启动了一个进程。该进程会自动启动一个“主线程”,然后主线程负责调用某个类的main 方法。所以main 方法的执行是在主线程中执行的。然后通过main 方法代码的执行可以启动其他的“分支线程”。所以,main 方法结束程序不一定结束,因为其他的分支线程有可能还在执行。

11.2 线程的生命周期

线程的生命周期存在五个状态:新建就绪运行阻塞死亡

JavaSE笔记 [全文字数7.1W]_第52张图片

  • 新建:当程序使用new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM 为其分配
    内存,并初始化其成员变量的值。

  • 就绪:当线程对象调用了start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和
    程序计数器,等待调度运行。

  • 运行:如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状
    态。

  • 阻塞:阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。
    直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状
    态。阻塞的情况分三种:

    • 等待阻塞(o.wait->等待对列):运行(running)的线程执行o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。
    • 同步阻塞(lock->锁池):运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM 会把该线程放入锁池(lock pool)中。
    • 其他阻塞(sleep/join):运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O 请求时,JVM 会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O
      处理完毕时,线程重新转入可运行(runnable)状态。
  • 死亡:线程会以下面三种方式结束,结束后就是死亡状态。

    • 正常结束:run()或call()方法执行完成,线程正常结束。
    • 异常结束:线程抛出一个未捕获的Exception 或Error。
    • 调用stop:直接调用该线程的stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。

11.3 两种线程实现方式

Java 虚拟机的主线程入口是main 方法,用户可以自己创建线程,创建方式有两种:

  • 继承Thread 类

  • 实现Runnable 接口(推荐使用Runnable 接口)

11.3.1 继承 Thread 类

Thread 类中创建线程最重要的两个方法为:

public void run();
public void start();

采用Thread 类创建线程,用户只需要继承Thread,覆盖Thread 中的run 方法,父类Thread 中的run 方法没有抛出异常,那么子类也不能抛出异常,最后采用start 启动线程即可。

案例演示

//不使用线程的情况
public class ThreadTest01 {
    public static void main(String[] args) {
        Processor p = new Processor();
        p.run();
        method1();
    }
    private static void method1() {
        System.out.println("--------method1()----------");
    }
}
class Processor {
    public void run() {
        for (int i=0; i<10; i++) {
        	System.out.println(i);
        }
    }
}

运行结果如下:

在这里插入图片描述

以上顺序输出相应的结果(属于串行),也就是run 方法完全执行完成后,才执行method1 方法,也就是method1 必须等待前面的方法返回才可以得到执行,这是一种“同步编程模型


//使用线程的情况
public class ThreadTest02 {
    public static void main(String[] args) {
        Processor p = new Processor();
        
        //手动调用该方法
        //不能采用run 来启动一个场景(线程),
        //run 就是一个普通方法调用
        //p.run();
        
        //采用start 启动线程,不是直接调用run
        //start 不是马上执行线程,而是使线程进入就绪
        //线程的真正执行是由Java 的线程调度机制完成的
        p.start();
        
        //只能启动一次
        //p.start();
        method1();
    }
    private static void method1() {
    	System.out.println("--------method1()----------");
    }
}
class Processor extends Thread {
    //覆盖Thread 中的run 方法,该方法没有异常
    //该方法是由java 线程掉机制调用的
    //我们不应该手动调用该方法
    public void run() {
        for (int i=0; i<10; i++) {
        	System.out.println(i);
        }
    }
}

运行结果如下:

JavaSE笔记 [全文字数7.1W]_第53张图片

通过输出结果大家会看到,没有顺序执行,而在输出数字的同时执行了method1()方法,如果从效率上看,采用多线程的示例要快些,因为我们可以看作他是同时执行的,mthod1()方法没有等待前面的操作完成才执行,这叫“异步编程模型

11.3.2 实现 Runnable 接口

其实Thread 对象本身就实现了Runnable 接口,但一般建议直接使用Runnable 接口来写多线程程序,因为接口会比类带来更多的好处。

案例演示

public class ThreadTest03 {
    public static void main(String[] args) {
        //Processor r1 = new Processor();
        Runnable r1 = new Processor();
        
        //不能直接调用run
        //p.run();
        Thread t1 = new Thread(r1);
        
        //启动线程
        t1.start();
        method1();
    }
    private static void method1() {
    	System.out.println("--------method1()----------");
    }
}
//实现Runnable 接口
class Processor implements Runnable {
    //实现Runnable 中的run 方法
    public void run() {
        for (int i=0; i<10; i++) {
        	System.out.println(i);
        }
    }
}

运行结果如下:

JavaSE笔记 [全文字数7.1W]_第54张图片

注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其它的类,更灵活。

11.3.3 两种方式比较

实现Runnable 接口相比于继承Thread 的优势:

​ 1):适合多个相同的程序代码的线程去处理同一个资源

​ 2):可以避免java中的单继承限制

​ 3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

​ 4):线程池只能放入实现Runnable或callable类线程,不能直接放入继承Thread类

注:java中每个程序的运行至少启动两个线程一个是main 线程,一个是垃圾收集线程(GC)

11.4 线程调度与控制

​ 通常我们的计算机只有一个CPU,CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。在单CPU 的机器上线程不是并行运行的,只有在多个CPU 上线程才可以并行运行。Java 虚拟机要负责线程的调度,取得CPU 的使用权,目前有两种调度模型:分时调度模型和抢占式调度模型,Java 使用抢占式调度模型。

均分式调度模型:所有线程轮流使用CPU 的使用权,平均分配每个线程占用CPU 的时间片。

抢占式调度模型:优先级高的线程获取CPU 的时间片相对多一些,如果线程的优先级相同,那么会随机选择一个。

11.4.1 线程的优先级

Java线程有10个优先级 1~10 Thread类有以下三个静态常量:

static int MAX_PRIORITY //线程可以具有最高优先级,取值为10
static int MIN_PRIORITY //线程的最低优先级,取值为1
static int NORM_PRIORITY //默认为优先级,取值5

方法:

//设置优先级
void setPriority(int newPriority);
//获取优先级
int getPriority();

每个线程都有默认的优先级,主线程默认优先级为 5。线程优先级有继承关系,A线程中创建了B线程,B和A优先级相同。

注:线程的优先级可以说是相对值, 若多个线程中若级别分别为10、5、3 ,10 的那个线程只能说相对其他线程有更高的优先级,10和5、5和3之间只有排名区别,没有量级的区别。

案例演示

public class ThreadTest04 {
    public static void main(String[] args) {
        Runnable r1 = new Processor();
        Thread t1 = new Thread(r1, "t1");
        
        //设置线程的优先级,线程启动后不能再次设置优先级
        //必须在启动前设置优先级
        //设置最高优先级
        t1.setPriority(Thread.MAX_PRIORITY);
        
        //启动线程
        t1.start();
        
        //取得线程名称
        //System.out.println(t1.getName());
        
        Thread t2 = new Thread(r1, "t2");
        
        //设置最低优先级
        t2.setPriority(Thread.MIN_PRIORITY);
        t2.start();
        System.out.println(Thread.currentThread().getName());
    }
}
class Processor implements Runnable {
    public void run() {
        for (int i=0; i<100; i++) {
        	System.out.println(Thread.currentThread().getName() + "," + i);
        }
    }
}

运行结果如下:

JavaSE笔记 [全文字数7.1W]_第55张图片

从以上输出结果应该看可以看出,优先级高的线程(t1)会得到的CPU 时间多一些,优先执行完成。

11.4.2 Thread.sleep 线程睡眠

​ sleep 设置休眠的时间,单位毫秒,当一个线程遇到sleep 的时候,就会睡眠,进入到阻塞状态,放弃CPU,腾出cpu 时间片,给其他线程用,所以在开发中通常我们会这样做,使其他的线程能够取得CPU 时间片,当睡眠时间到达了,线程会进入可运行状态,得到CPU 时间片继续执行,如果线程在睡眠状态被中断了,将会抛出IterruptedException

方法:

static void sleep(long millis);

案例演示

public class ThreadTest05 {
    public static void main(String[] args) {
        Runnable r1 = new Processor();
        Thread t1 = new Thread(r1, "t1");
        t1.start();
        Thread t2 = new Thread(r1, "t2");
        t2.start();
    }
}
class Processor implements Runnable {
    public void run() {
        for (int i=0; i<100; i++) {
            System.out.println(Thread.currentThread().getName() + "," + i);
            if (i % 10 == 0) {
                try {
                    //睡眠100 毫秒,主要是放弃CPU 的使用,将CPU 时间片交给其他线程使用
                    Thread.sleep(100);
                }catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
sleep与wait区别
  1. 对于sleep()方法,我们首先要知道该方法是属于Thread 类中的。而wait()方法,则是属于Object 类中的。
  2. sleep()方法导致了程序暂停执行指定的时间,让出cpu 该其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。
  3. 在调用sleep()方法的过程中,线程不会释放对象锁
  4. 而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

11.4.3 Thread.yield 线程让步

它与sleep()类似,只是不能由用户指定暂停多长时间,并且yield()方法只能让同优先级的线程有执行的机会。

方法:

static void yield();

案例演示

public class ThreadTest06 {
    public static void main(String[] args) {
        Runnable r1 = new Processor();
        Thread t1 = new Thread(r1, "t1");
        t1.start();
        Thread t2 = new Thread(r1, "t2");
        t2.start();
    }
}
class Processor implements Runnable {
    public void run() {
        for (int i=0; i<100; i++) {
            System.out.println(Thread.currentThread().getName() + "," + i);
            if (i % 10 == 0) {
                System.out.println("--------------");
                //采用yieid 可以将CPU 的使用权让给同一个优先级的线程
                Thread.yield();
            }
        }
    }
}

11.4.4 Thread.join 线程插入

​ 当前线程可以调用另一个线程的join 方法,调用后当前线程会被阻塞不再执行,直到被调用的线程执行完毕,当前线程才会执行。

方法:

void join();
void join(long millis); //等待这个线程死亡最多 millis毫秒。

案例演示

public class ThreadTest07 {
    public static void main(String[] args) {
        Runnable r1 = new Processor();
        Thread t1 = new Thread(r1, "t1");
        t1.start();
        try {
        	t1.join();
        }catch(InterruptedException e) {
        	e.printStackTrace();
        }
        System.out.println("------main end-------");
    }
}
class Processor implements Runnable {
    public void run() {
        for (int i=0; i<10; i++) {
        	System.out.println(Thread.currentThread().getName() + "," + i);
        }
    }
}

11.4.5 Thread.interrupt 线程中断

如果我们的线程正在睡眠,可以采用interrupt 进行中断。

方法:

void interrupt();
static boolean interrupted(); //测试当前线程是否中断。

案例演示

public class ThreadTest08 {
    public static void main(String[] args) {
        Runnable r1 = new Processor();
        Thread t1 = new Thread(r1, "t1");
        t1.start();
        try {
            //设置为500 毫秒,没有出现中断异常,因为500 毫秒之后再次调用t1.interrupt()时,
            //此时的睡眠线程已经执行完成。如果sleep 的时间设置的小一些,会出现中断异常
            Thread.sleep(500);
        }catch(Exception e) {
        	e.printStackTrace();
        }
        //中断睡眠中的线程
        t1.interrupt();
    }
}
class Processor implements Runnable {
    public void run() {
        for (int i=1; i<100; i++) {
            System.out.println(Thread.currentThread().getName() + "," + i);
            if (i % 50 == 0) {
                try {
                    Thread.sleep(200);
                }catch(Exception e) {
                    System.out.println("-------中断-------");
                    break;
                }
            }
        }
    }
}

11.4.6 推荐的停止线程方式

通常定义一个标记,来判断标记的状态停止线程的执行。

案例演示

public class ThreadTest09 {
    public static void main(String[] args) {
        //Runnable r1 = new Processor();
        Processor r1 = new Processor();
        Thread t1 = new Thread(r1, "t1");
        t1.start();
        try {
        	Thread.sleep(20);
        }catch(Exception e) {}
        //停止线程
        r1.setFlag(true);
    }
}
class Processor implements Runnable {
    //线程停止标记,true 为停止
    private boolean flag;
    
    public void run() {
        for (int i=1; i<100; i++) {
            System.out.println(Thread.currentThread().getName() + "," + i);
            //为true 停止线程执行
            if (flag) {
            	break;
            }
        }
    }
    
    public void setFlag(boolean flag) {
    	this.flag = flag;
    }
}

11.5 Thread类常用方法

//启动线程,然后由jvm调用该线程的run方法
void start();
void run();

//返回当前活跃的线程数
static int activeCount();

//得到当前线程
static Thread currentThread();

//返回此线程的标识符
long getId();
//设置此线程的名称
void setName(String name);
String getName();

//设置优先级
void setPriority(int newPriority);
//获取优先级
int getPriority();

//测试这个线程是否活着
boolean isAlive();

//测试这个线程是否是守护线程
boolean isDaemon();
//设置一个线程为守护线程
void setDaemon(boolean on);

//返回此线程的字符串表示,包括线程的名称,优先级和线程组
String toString();

11.6 线程锁

11.6.1 乐观锁

​ 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

​ java 中的乐观锁基本都是通过CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败

11.6.2 悲观锁

​ 悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block 直到拿到锁。java 中的悲观锁就是Synchronized,AQS 框架下的锁则是先尝试cas 乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。

11.6.3 自旋锁

​ 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗

​ 线程自旋是需要消耗cup 的,说白了就是让cup 在做无用功,如果一直获取不到锁,那线程也不能一直占用cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。

​ 如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

自旋锁的优缺点

​ 自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
​ 但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu 做无用功,占着XX 不XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup 的线程又不能获取到cpu,造成cpu 的浪费。所以这种情况下我们要关闭自旋锁。

自旋锁时间阈值(1.6 引入了适应性自旋锁)

​ 自旋锁的目的是为了占着CPU 的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU 资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!

​ JVM 对于自旋周期的选择,jdk1.5 这个限度是一定的写死的,在1.6 引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM 还针对当前CPU 的负荷情况做了较多的优化,如果平均负载小于CPUs 则一直自旋,如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞,如果CPU 处于节电模式则停止自旋,自旋时间的最坏情况是CPU的存储延迟(CPU A 存储了一个数据,到CPU B 得知这个数据直接的时间差),自旋时会适当放弃线程优先级之间的差异。

11.6.4 Synchronized 同步锁

​ synchronized 它可以把任意一个非NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁

Synchronized 作用范围

  1. 作用于方法时,锁住的是对象的实例(this)
  2. 当作用于静态方法时,锁住的是Class 实例,又因为Class 的相关数据存储在永久带PermGen(jdk1.8 则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
  3. synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

Synchronized 基本使用

  1. 修饰普通方法:

    public sychronized void text(){
    	//方法体
    }
    
  2. 修饰代码块:

    public void text{
    	//锁的是括号的对象
    	sychronized(this){
    		
    	}
    }
    
  3. 修饰静态方法(锁住的是类):

    public class Person(){
    	//锁住的是调用这个方法的对象的所属类
    	public static synchronized void text(){
    	
    	}
    }
    

Synchronized 核心组件

​ 1) Wait Set:哪些调用wait 方法被阻塞的线程被放置在这里;

​ 2) Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;

​ 3) Entry List:Contention List 中那些有资格成为候选资源的线程被移动到Entry List 中

​ 4) OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck

​ 5) Owner:当前已经获取到所资源的线程被称为Owner;

​ 6) !Owner:当前释放锁的线程。

Synchronized 实现

JavaSE笔记 [全文字数7.1W]_第56张图片

  1. JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList 会被大量的并发线程进行CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到EntryList 中作为候选竞争线程。
  2. Owner 线程会在unlock 时,将ContentionList 中的部分线程迁移到EntryList 中,并指定EntryList 中的某个线程为OnDeck 线程(一般是最先进去的那个线程)。
  3. Owner 线程并不直接把锁传递给OnDeck 线程,而是把锁竞争的权利交给OnDeck,OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”。
  4. OnDeck 线程获取到锁资源后会变为Owner 线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner 线程被wait 方法阻塞,则转移到WaitSet 队列中,直到某个时刻通过notify或者notifyAll 唤醒,会重新进去EntryList 中。
  5. 处于ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用pthread_mutex_lock 内核函数实现的)。
  6. Synchronized 是非公平锁。Synchronized 在线程进入ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck 线程的锁资源。
  7. 每个对象都有个monitor 对象,加锁就是在竞争monitor 对象,代码块加锁是在前后分别加上monitorenter 和monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
  8. synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。
  9. Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的Java1.7 与1.8 中,均对该关键字的实现机理做了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
  10. 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;
  11. JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。

11.7 守护线程

​ 从线程分类上可以分为:用户线程(以上讲的都是用户线程),另一个是守护线程。守护线程是这样的,所有的用户线程结束生命周期,守护线程才会结束生命周期,只要有一个用户线程存在,那么守护线程就不会结束,例如java 中著名的垃圾回收器就是一个守护线程,只有应用程序中所有的线程结束,它才会结束。

  1. 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
  2. 用法:通过setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在线程对象创建之前用线程对象的setDaemon 方法。
  3. 生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则JVM 不会退出。
  4. 注意在Daemon 线程中产生的新线程也是Daemon 的线程则是JVM 级别的,以Tomcat 为例,如果你在Web 应用中启动一个线程,这个线程的生命周期并不会和Web 应用程序保持同步。也就是说,即使你停止了Web 应用,这个线程依旧是活跃的。
  5. 示例:垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM 上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

案例演示

public class DaemonThreadTest02 {
    public static void main(String[] args) {
        Runnable r1 = new Processor();
        Thread t1 = new Thread(r1, "t1");
        //将当前线程修改为守护线程
        //在线程没有启动时可以修改以下参数
        t1.setDaemon(true);
        t1.start();
        for (int i=0; i<10; i++) {
            System.out.println(Thread.currentThread().getName() + ", " + i);
        }
        System.out.println("主线程结束!!!");
    }
}
class Processor implements Runnable {
    public void run() {
        for (int i=0; i<10; i++) {
            System.out.println(Thread.currentThread().getName() + ", " + i);
        }
    }
}

JavaSE笔记 [全文字数7.1W]_第57张图片

设置为守护线程后,当主线程(main)结束后,守护线程并没有把所有的数据输出完就结束了。也即是说守护线程是为用户线程服务的,当用户线程全部结束,守护线程会自动结束。

第十二章 反射

12.1 反射的基本概念

​ 反射的概念是由Smith 在1982 年首次提出的,主要是指程序可以访问、检测和修改它本身状态或行为的一种能力, 并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。Java 中,反射是一种强大的工具。它使您能够创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码链接。反射允许我们在编写与执行时,使我们的程序代码能够接入装载到JVM 中的类的内部信息,而不是源代码中选定的类协作的代码。这使反射成为构建灵活的应用的主要工具。但需注意的是:如果使用不当,反射的成本很高。

什么是反射机制

​ 在Java 中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java 语言的反射机制。

12.2 反射的使用

12.2.1 反射使用场合

在Java 程序中许多对象在运行是都会出现两种类型:编译时类型和运行时类型。编译时的类型由声明对象时实用的类型来决定,运行时的类型由实际赋值给对象的类型决定。如:

Person p=new Student();

其中编译时类型为Person,运行时类型为Student

编译时类型无法获取具体方法。程序在运行时还可能接收到外部传入的对象,该对象的编译时类型为Object,但是程序有需要调用该对象的运行时类型的方法。

为了解决这些问题,程序需要在运行时发现对象和类的真实信息。然而,如果编译时根本无法预知该对象和类属于哪些类,程序只能依靠运行时信息来发现该对象和类的真实信息,此时就必须使用到反射了。

12.2.2 反射API

反射API 用来生成JVM 中的类、接口或则对象的信息。

  • Class 类:反射的核心类,可以获取类的属性,方法等信息。
  • Field 类:Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。
  • Method 类:Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行方法。
  • Constructor 类:Java.lang.reflec 包中的类,表示类的构造方法。

12.2.3 使用步骤

  1. 获取想要操作的类的Class 对象,他是反射的核心,通过Class 对象我们可以任意调用类的方法。
  2. 调用Class 类中的方法,既就是反射的使用阶段。
  3. 使用反射API 来操作这些信息。

12.2.4 获取class对象的3种方法

  • 调用某个对象的getClass()方法

    Person p=new Person();
    Class clazz=p.getClass();
    
  • 调用某个类的class 属性来获取该类对应的Class 对象

    Class clazz=Person.class;
    
  • 使用Class 类中的forName()静态方法(最安全/性能最好

    Class clazz=Class.forName("类的全路径"); 	//(最常用)
    

当我们获得了想要操作的类的Class 对象后,可以通过Class 类中的方法获取并查看该类中的方法
和属性。

public class Test{
    public static void main(String[] args){
        //获取Person 类的Class 对象
        Class clazz=Class.forName("reflection.Person");

        //获取Person 类的所有方法信息
        Method[] method=clazz.getDeclaredMethods();
        for(Method m:method){
            System.out.println(m.toString());
        }

        //获取Person 类的所有成员属性信息
        Field[] field=clazz.getDeclaredFields();
        for(Field f:field){
        	System.out.println(f.toString());
        }
        
        //获取Person 类的所有构造方法信息
        Constructor[] constructor=clazz.getDeclaredConstructors();
        for(Constructor c:constructor){
        	System.out.println(c.toString());
        }
    }
}

12.2.5 创建对象的两种方法

Class 对象的newInstance()

​ 使用Class 对象的newInstance()方法来创建该Class 对象对应类的实例,但是这种方法要求
该Class 对象对应的类有默认的空构造器。

调用Constructor 对象的newInstance()

​ 先使用Class 对象获取指定的Constructor 对象,再调用Constructor 对象的newInstance()
方法来创建Class 对象对应类的实例,通过这种方法可以选定构造方法创建实例。

public class Test{
    public static void main(String[] args){
        //获取Person 类的Class 对象
        Class clazz = Class.forName("reflection.Person");
        //使用.newInstane 方法创建对象
        Person p = (Person)clazz.newInstance();
        //获取构造方法并创建对象
        Constructor c = clazz.getDeclaredConstructor(
            	String.class,String.class,int.class);
        //创建对象并设置属性
        Person p1 = (Person)c.newInstance("李四","男",20);
    }
}

12.3 反射的缺点

  • 在处理反射时安全性是一个较复杂的问题。反射经常由框架型代码使用,由于这一点,我们可能希望框架能够全面介入代码,无需考虑常规的介入限制。但是,在其它情况下,不受控制的介入会带来严重的安全性风险,例如当代码在不值得信任的代码共享的环境中运行时。

  • 性能问题。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且它满足我们的要求。用于字段和方法接入时反射要远慢于直接代码。性能问题的程度取决于程序中是如何使用反射的。如果它作为程序运行中相对很少涉及的部分,缓慢的性能将不会是一个问题。

  • 使用反射会模糊程序内部实际要发生的事情。程序人员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术会带来维护问题。反射代码比相应的直接代码更复杂。解决这些问题的最佳方案是保守地使用反射——仅在它可以真正增加灵活性的地方——记录其在目标类中的使用。

你可能感兴趣的:(编程学习笔记,java)