文章目录
- java程序运行流程
- JDK、JRE、JVM的关系
- java数据类型
- 基本数据类型
- 基本数据类型之间的转换
- 引用数据类型
- 数组
- 类
- Java Bean
- 重写 equals和hashcode
- 接口
- 接口与类之间的关系
- 内置包装类
- 装箱和拆箱
- 包装类的应用
- Object类
- toString()
- equals()方法
- getClass() 方法
- 接收任意引用类型的对象
- Integer类
- Float类
- 字符串类
- String类常用API
- 集合
- 迭代器iterator
- Collection
- List
- List实现ArrayList
- List实现LinkedList
- ListIterator
- List实现vector
- List实现Stack
- Queue
- Deque
- ArrayDeque
- PriorityQueue
- Set
- HashSet
- LinkedHashSet
- TreeSet
- 双列集合Map
- Map的4种遍历方式
- Map实现HashMap
- Map实现LinkedHashMap
- Map实现TreeMap
- Stream流
- 获取stream流的方式
- 常用API
- 异常
- 编译时异常
- 运行时异常
- 自定义异常
- 流对象
- java流
- 输入流
- 输出流
- File类常用API
- 创建和删除文件
- 遍历目录
- 搜索文件
- 字符集
- IO流
- FileInputStream
- FileOutPutStream
- 文件拷贝案例
- FileReader
- FileWriter
- 资源释放
- try-catch-finally
- try-with-resource
- 缓冲流
- 转换流
- 对象序列化和反序列化
- 序列化ObjectOutputStream流
- 反序列化ObjectInputStream流
- 对象序列化和反序列化的版本兼容问题
- 序列化文件的几种格式
- 打印流
- PrintStream和PrintWriter的区别
- Properties
java程序运行流程
源代码.java
-> 经过编译器javac.exe
编译为java字节码 源代码.class
-> 编译完成的代码可以在 java虚拟机JVM
中运行。
JDK、JRE、JVM的关系
-
JDK:(java开发工具箱)包含JAVA编译器工具和JRE。常见的java编辑工具有:
- javac.exe:将java源码编译为java.class字节码的工具
- java.exe:Java解释器,启动 JVM(Java虚拟机),将 .class 文件一行一行地解释成机器指令执行。(由 Java 虚拟机对字节码进行解释和执行)
-
JRE(java运行环境):它主要包含两个部分,jvm 的标准实现和 Java 的一些基本类库。它相对于 jvm 来说,多出来的是一部分的 Java 类库。
-
JVM:java虚拟机,它只认识 xxx.class 这种类型的文件,它能够将 class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作,jvm 是 Java 能够跨平台的核心。
所以三者的关系是JDK(java开发工具箱)包含JRE(java运行环境),JRE包含JVM(java虚拟机)
java数据类型
基本数据类型
Java中定义了四类/八种基本数据类型
- 布尔型 boolean:只允许赋值
true
或false
- 字符型 char:java字符采用Unicode编码,每个字符占用两个字节,16位。
- 整数型---- byte, short, int, long,占用字节数和可表示范围依次翻倍
- 浮点数型---- float, double
整数类型byte、short、int、long占用的空间倍数上升。
默认的小数类型为double,如果要给浮点数类型赋值,需要在小数后边+f或F
表示浮点数。
基本数据类型之间的转换
自动类型转换:表示数据范围从小到大。
强制类型转换:如果是浮点数类型转为整数会去掉小数部分,丢失精度;
如果是整数类型之间或者浮点数类型之间由高范围转为低范围,会直接丢弃高位部分,导致原本的超出范围的正数可能出现负数
。
引用数据类型
引用数据类型建立在基本数据类型的基础上,包括数组、类和接口。
引用类型还有一种特殊的 null 类型。在实际开发中,程序员可以忽略 null 类型,假定 null 只是引用类型的一个特殊直接量。
注意:空引用(null)只能被转换成引用类型,不能转换成基本类型,因此不要把一个 null 值赋给基本数据类型的变量。
数组
长度固定,存放同一种数据类型的连续存储空间。
类
类是具有相同属性和服务的一组对象的集合
。为属于该类的所有对象提供了统一的抽象描述,其内部包括属性和服务(功能)两个主要部分。
Java Bean
在java编程中,在编写一个具体的类时通常会包含:
- 一个默认的无参构建器,一个有参构造器
- 需要被序列化并且实现了序列化接口,一般是重写toString,将属性展示出
- 可能有一系列的可读写属性,并且一般是private的
- 有一系列的get、set方法
符合上边条件的类称为一个java bean。
java bean最大的特征是私有的属性,其作用也就是把一组数据组合成一个特殊的类便于传输。 例如:将所需要的物品放到箱子指定的格子当中,然后快递员将打包好的箱子发出去。这个箱子就是 Java Bean。
重写 equals和hashcode
equals()定义在Object类中,其底层是通过 ==来比较,也就是说通过比较两个对象的内存地址是否相同来判断是否是同一个对象。
很多对象都会重写equals这个方法,因为我们实际上想要的结果是比较两个对象的内容是否一致。
例如String类中就重写了equals:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
hashcode是由对象的地址计算得到的哈希码,这个hashcode值决定了对象存放在哈希表中的位置。
常见的散列表实现方式:用数组作为哈希函数的输出域,输入值经过哈希函数计算后得到哈希值。然后根据哈希值,在数组种找到对应的存储单元。当发生冲突时,对应的存储单元以链表的形式保存冲突的数据(链地址法)。
hashcode可以起到一定的判重作用,即hashcode值不一样,对象一定不一样。但hashcode一样的对象,可能也不相同,因为有哈希冲突!!!
按照重写equals的思路,我们判断两个对象是否一致,是比较他们的内容是否一致,那么根据地址计算的hashcode,也是不符合要求的!!!
因此重写equals()方法,必须同时重写hashcode()方法。
例如在下边这个类中,重写的equals,是在比较类的三个属性size、color、index是否相同,而重写的hashcode,是根据类的三个属性size、color、index计算得到的hashcode。
public class Card {
private String size;
private String color;
private int index;
public Card(){
}
public Card(String size, String color, int index) {
this.size = size;
this.color = color;
this.index = index;
}
@Override
public String toString() {
return size +
","+ color;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Card card = (Card) o;
return index == card.index && Objects.equals(size, card.size) && Objects.equals(color, card.color);
}
@Override
public int hashCode() {
return Objects.hash(size, color, index);
}
}
接口
接口(Interface),在JAVA编程语言中是一个抽象类型,是抽象方法的集合(接口里面都是抽象方法)
,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。
接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法,当然接口中也可以定义不需要实现的常量或者static方法。
接口是为了突破java单继承的局限性而来的。
接口与类之间的关系
-
类与类
- 类和类之间是继承关系,而且只允许单根继承,子类继承父类后,可以使用父类除封装之外的所有功能。
- 子类可以修改父类的原有功能,会发生方法重写。
- 方法重写需要满足方法头要相同,方法要用@Override标识。
- 重写改变的是方法体,是把子类的方法体改成新的业务
-
接口与接口之间
- 接口也可以继承
- 子接口可以使用父接口的所有功能。
- 接口是为了突破java单继承的局限性而来的。
- 接口里没有构造方法,没有变量只有常量,jdk1.8后可以有default或者static的普通方法。
- 接口可以多继承,多实现,还可以在继承的同时多实现(先继承再实现)。
-
类与接口之间
- 类和接口是实现关系,并且允许多实现
- 当实现类实现了接口后,需要重写接口里的所有方法,否则就包含 抽象方法,变成了一个抽象类。
内置包装类
在 Java 的设计中提倡一种思想,即一切皆对象。但是从数据类型的划分中,我们知道 Java 中的数据类型分为基本数据类型和引用数据类型,但是基本数据类型怎么能够称为对象呢?于是 Java 为每种基本数据类型分别设计了对应的类,称之为包装类(Wrapper Classes)
装箱和拆箱
基本数据类型转换为包装类的过程称为装箱,例如把 int 包装成 Integer 类的对象;包装类变为基本数据类型的过程称为拆箱,例如把 Integer 类的对象重新简化为 int。
Java 1.5 版本之前必须手动拆箱装箱,之后可以自动拆箱装箱
,也就是在进行基本数据类型和对应的包装类转换时,系统将自动进行装箱及拆箱操作,不用在进行手工操作,为开发者提供了更多的方便。例如:
public class Demo {
public static void main(String[] args) {
int m = 500;
Integer obj = m; // 自动装箱
int n = obj; // 自动拆箱
System.out.println("n = " + n);
Integer obj1 = 500;
System.out.println("obj等价于obj1返回结果为" + obj.equals(obj1));
}
}
包装类的应用
1) 实现 int 和 Integer 的相互转换
可以通过 Integer 类的构造方法将 int 装箱,通过 Integer 类的 intValue 方法将 Integer 拆箱。
public class Demo {
public static void main(String[] args) {
int m = 500;
Integer obj = new Integer(m); // 手动装箱
int n = obj.intValue(); // 手动拆箱
System.out.println("n = " + n);
Integer obj1 = new Integer(500);
System.out.println("obj等价于obj1的返回结果为" + obj.equals(obj1));
}
}
2) 将字符串转换为数值类型
① Integer 类(String 转 int 型)
int parseInt(String s);
② Float 类(String 转 float 型)
float parseFloat(String s)
注意:使用以上两种方法时,字符串中的数据必须由数字组成,否则转换时会出现程序错误。
public class Demo {
public static void main(String[] args) {
String str1 = "30";
String str2 = "30.3";
// 将字符串变为int型
int x = Integer.parseInt(str1);
// 将字符串变为float型
float f = Float.parseFloat(str2);
System.out.println("x = " + x + ";f = " + f);
}
}
3) 将整数转换为字符串
Integer 类有一个静态的 toString() 方法,可以将整数转换为字符串。例如:
public class Demo {
public static void main(String[] args) {
int m = 500;
String s = Integer.toString(m);
System.out.println("s = " + s);
}
}
Object类
Object 是 Java 类库中的一个特殊类,也是所有类的父类。也就是说,Java 允许把任何类型的对象赋给 Object 类型的变量。
当一个类被定义后,如果没有指定继承的父类,那么默认父类就是 Object 类
由于 Java 所有的类都是 Object 类的子类,所以任何 Java 对象都可以调用 Object 类的方法。常见的方法如表 所示。其中,toString()、equals() 方法和 getClass() 方法在 Java 程序中比较常用。
toString()
Object 类的 toString() 方法返回
“运行时类名@十六进制哈希码”格式的字符串
但很多类都重写了 Object 类的 toString() 方法
,用于返回可以表述该对象信息的字符串。
// 定义Demo类,实际上继承Object类
class Demo {
}
public class ObjectDemo01 {
public static void main(String[] args) {
Demo d = new Demo(); // 实例化Demo对象
System.out.println("不加toString()输出:" + d);
System.out.println("加上toString()输出:" + d.toString());
}
}
不加toString()输出:Demo@15db9742
加上toString()输出:Demo@15db9742
从程序的运行结果可以清楚的发现,加和不加 toString() 的最终输出结果是一样的,也就是说对象输出时一定会调用 Object 类中的 toString() 方法打印内容
equals()方法
在前面学习字符串比较时,曾经介绍过两种比较方法,分别是==
运算符和 equals()
方法.
==
运算符是比较两个引用变量是否指向同一个实例- equals() 方法是比较两个对象的内容是否相等,通常字符串的比较只是关心内容是否相等。
getClass() 方法
getClass() 方法返回对象所属的类,是一个 Class 对象。通过 Class 对象可以获取该类的各种信息,包括类名、父类以及它所实现接口的名字等。
接收任意引用类型的对象
既然 Object 类是所有对象的父类,则所有的对象都可以向 Object 进行转换,在这其中也包含了数组和接口类型,即一切的引用数据类型都可以使用 Object 进行接收。
interface A {
public String getInfo();
}
class B implements A {
public String getInfo() {
return "Hello World!!!";
}
}
public class ObjectDemo04 {
public static void main(String[] args) {
// 为接口实例化
A a = new B();
// 对象向上转型
Object obj = a;
// 对象向下转型
A x = (A) obj;
System.out.println(x.getInfo());
}
}
通过以上代码可以发现,虽然接口不能继承一个类,但是依然是 Object 类的子类,因为接口本身是引用数据类型,所以可以进行向上转型操作。
Integer类
Float类
字符串类
- String表示字符串类型,属于 引用数据类型,不属于基本数据类型。
- 在java中随便使用 双引号括起来 的都是String对象。
- java中规定,双引号括起来的字符串,是 不可变 的,也就是说"abc"自出生到最终死亡,不可变,不能变成"abcd",也不能变成"ab"
- 在JDK当中双引号括起来的字符串,例如:“abc” "def"都是直接存储在“方法区”的“字符串常量池”当中的。这是
因为字符串在实际的开发中使用太频繁。为了执行效率,所以把字符串放到了方法区的字符串常量池当中。
例子1
public class StringTest01 {
public static void main(String[] args) {
// 这两行代码表示底层创建了3个字符串对象,都在字符串常量池当中。
String s1 = "abcdef";
String s2 = "abcdef" + "xy";
// 分析:这是使用new的方式创建的字符串对象。这个代码中的"xy"是从哪里来的?
// 凡是双引号括起来的都在字符串常量池中有一份。
// new对象的时候一定在堆内存当中开辟空间。
String s3 = new String("xy");
}
}
例子2
public class StringTest02 {
public static void main(String[] args) {
String s1 = "hello";
// "hello"是存储在方法区的字符串常量池当中
// 所以这个"hello"不会新建。(因为这个对象已经存在了!)
String s2 = "hello";
// == 双等号比较的是变量中保存的内存地址
System.out.println(s1 == s2); // true
String x = new String("xyz");
String y = new String("xyz");
// == 双等号比较的是变量中保存的内存地址
System.out.println(x == y); //false
}
}
由此可见:字符串对象之间的比较不能使用“== ”。应该调用String类的equals方法
String类常用API
集合
集合区分于数组,它的大小是可变的。
数组可以存储基本数据类型和引用数据类型,而集合中只能存储引用数据类型(存储的为对象的内存地址)
单列集合Collection:Queue、List、Set
双列集合:Map
迭代器iterator
迭代器属于设计模式之一,迭代器模式提供了一种方法顺序访问一个聚合对象中的各个元素,而不保留对象的内部表示。iterator仅用于遍历集合,本身不存放对象
所有实现了Collection接口的集合类都有一个iterator(),方法,返回一个实现了iterator接口的对象。
iterator接口包含三个方法:
- hasNext():迭代器当前位置下一个是否还有元素
- next():移动迭代器到下一个位置,返回所在位置的元素
- remove:删除当前迭代器当前位置的元素
Collection接口返回的迭代器起初位置
在集合首元素的前一个位置
.
在idea中使用itit
快捷输出迭代器遍历方式:
LinkedList<Integer> L1 = new LinkedList<>();
Collections.addAll(L1,111,222,333);
Iterator<Integer> it = L1.iterator();
while (it.hasNext()) {
Integer next = it.next();
it.remove();
System.out.println(next);
}
System.out.println(L1);
上边代码会依次遍历集合,打印。
它的原理如下图:
1、起初it在集合首元素的前一个位置:
2、调用next,移动迭代器到下一个位置,并返回当前位置的元素。
3、调用迭代器的remove,删除当前位置的元素,使用迭代器删除时会自动更新迭代器和集合(后边的元素前移)
4、当迭代器移动到最后一个位置时,因为没有下一个元素,退出循环。
5、循环外部打印,结果为空列表
Collection
单列集合的顶层接口,既然是接口就不能直接使用,需要通过实现类!
List
List表示一串有序的集合,和Collection接口含义不同的是List突出有序的含义。
List其实比Collection多了添加方法add和addAll查找方法get,indexOf,set等方法,并且支持index下标操作。
Collection和List最大的区别就是Collection是无序的,不支持索引操作,而List是有序的,支持索引操作
List实现ArrayList
ArrayList底层就是一个Object[]数组,ArrayList底层数组默认初始化容量为10;
1、jdk1.8 中 ArrayList 底层先创建⼀个⻓度为 0 的数组
2、当第⼀次添加元素(调⽤ add() ⽅法)时,会初始化为⼀个⻓度为 10 的数组
当 ArrayList 中的容量使⽤完之后,则需要对容量进⾏扩容:
1、ArrayList 容ᰁ使⽤完后,会“⾃动”创建容量更⼤的数组,并将原数组中所有元素拷⻉过去,这会导致效率降低。
2、优化:可以使⽤构造⽅法 ArrayList (int capacity) 或 ensureCapacity(int capacity) 提供⼀个初始化容量,避免刚开始就⼀直扩容,造成效率较低。
ArrayList 构造⽅法:
- ArrayList():创建⼀个初始化容ᰁ为 10 的空列表
- ArrayList(int initialCapacity):创建⼀个指定初始化容ᰁ为 initialCapacity 的空列表
- ArrayList(Collection<? extends E> c):创建⼀个包含指定集合中所有元素的列表
ArrayList 特点:
- 优点:
- 向 ArrayList 末尾添加元素(add() ⽅法)时,效率较⾼
- 查询效率⾼
- 缺点:
- 扩容会造成效率较低(可以通过指定初始化容量,在⼀定程度上对其进⾏改善)
- 另外数组⽆法存储⼤数据量(因为很难找到⼀块很⼤的连续的内存空间)
- 向 ArrayList 中间添加元素(add(int index)),需要移动元素,效率较低 ,如果增/删操作较多,可考虑改⽤链表
List实现LinkedList
LinkedList 特点
数据结构: LinkedList 底层是⼀个双向链表
优点: 增/删效率⾼
缺点: 查询效率较低
LinkedList 也有下标,但是内存不⼀定是连续的(类似C++重载[]符号,将循位置访问模拟为循秩访问)
LinkedList 可以调⽤ get(int index) ⽅法,返回链表中第 index 个元素
但是,每次查找都要从头结点开始遍历
ListIterator
ListIterator:列表迭代器,iterator接口的子接口。有如下功能:
- 允许双向移动迭代器,即可以向前或者向后两个方向遍历List
- 在遍历时修改List元素的值
- 遍历时获取迭代器当前位置
- LinkedList.add()方法只能将数据添加到链表末尾,如果要将对象添加到链表的中间位置,就需要使用 ListIterator
ListIterator的接口定义如下:
ListIterator 有两种获取方式
- List.listIterator(),位置在起始位置前一个
- List.listIterator(int location),位置在索引位置的前一个。
使用ListIterator可以在指定位置插入元素:
因为使用迭代器操作集合(插入或者删除)时,迭代器和集合会更新,所以不会出错。
private static void listIteratorDemo2(List list) {
list.add("abc1");
list.add("abc2");
list.add("abc3");
list.add("abc4");
// 获取列表迭代器
ListIterator it=list.listIterator();
while(it.hasNext())
{
Object obj=it.next();
if(obj.equals("abc2"))
{
it.add("hello");
}
System.out.println(obj);
}
System.out.println(list);
}
List实现vector
vector 是线程安全的,它是操作方法都被synchronize修饰。
vector实际开发使用较少
List实现Stack
Stack也是List接口的实现类之一,和Vector一样,因为性能原因,更主要在开发过程中很少用到栈这种数据结构,不过栈在计算机底层是一种非常重要的数据结构。
栈的最大特点就是后进先出。
Stack<String> strings = new Stack<>();
strings.push("aaa");
strings.push("bbb");
strings.push("ccc");
System.err.println(strings.pop());
复制代码
Queue
Queue是继承自Collection的接口,是一种先进先出的数据结构。
package java.util;
public interface Queue<E> extends Collection<E> {
//集合中插入元素
boolean add(E e);
//队列中插入元素
boolean offer(E e);
//移除元素,当集合为空,抛出异常
E remove();
//移除队列头部元素并返回,如果为空,返回null
E poll();
//查询集合第一个元素,如果为空,抛出异常
E element();
//查询队列中第一个元素,如果为空,返回null
E peek();
}
Deque
Deque是继承自Queue的接口,相比Queue,支持两端弹出和压入元素。
Deque英文全称是Double ended queue,也就是俗称的双端队列。就是说对于这个队列容器,既可以从头部插入也可以从尾部插入,既可以从头部获取,也可以从尾部获取。
ArrayDeque
ArrayDeque是Java中基于数组实现的双端队列,在Java中Deque的实现有LinkedList和ArrayDeque,正如它两的名字就标志了它们的不同,LinkedList是基于双向链表实现的,而ArrayDeque是基于数组实现的。
PriorityQueue
PriorityQueue是Java中唯一一个Queue接口的直接实现,如其名字所示,优先队列,其内部支持按照一定的规则对内部元素进行排序。
Set
如果说List对集合加了有序性的化,那么Set就是对集合加上了唯一性。
Set的特点在于不可存放重复元素,没有索引,不能使用普通的for循环遍历,需要使用迭代器,或者for each遍历。
Set集合的功能基本上与Collection的API是一致的。
HashSet
无序、不重复、无索引
HashSet集合,底层采用哈希表存储数据
- jdk8之前,底层使用数组+链表组成
- jdk8之后,底层采用数组+链表+红黑树组成
数组区间作为哈希函数的输出域,当发生哈希冲突时,采用链表的形式,将新的元素挂在数组的下方。当冲突很多时,就可能导致链表过长,导致查询速度变慢,因此jdk8后边的版本中,当链表长度超过8时,会将链表重新组织为红黑树。
LinkedHashSet
有序、不重复、无索引
底层数据结构依然是哈希表,只是每个元素额外多了一个双链表机制,记录存储的顺序。
TreeSet
可排序(自定义比较规则)、不重复、无索引
TreeSet集合是一定要排序的,底层是基于红黑树的数据结构。
如果要使用TreeSet存放自定义数据类型,一定要自定义比较规则。
自定义比较规则的方式:
- 方式1:让自定义类(如学生类)实现Comparable接口,重写compareTo方法,来定定制比较规则
- 方式2:TreeSet集合含有参数构造器,可以设置Comparable接口对应的比较器对象,来定制比较规则。
即:因为TreeSet要保证唯一性,因此当比较规则返回0的时候,认为元素重复,会将重复元素去除,然而实际应用中,可能比较规则只针对于对象的某一个属性,对象的某一个属性相同,不能认为,对象重复,因此,注意过滤掉返回0的情况。
双列集合Map
Map是一种键值对的结构,就是常说的Key-Value结构,一个Map就是很多这样K-V键值对组成的,一个K-V结构我们将其称作Entry,在Java里,Map是用的非常多的一种数据结构。
Map的4种遍历方式
1)在 for 循环中使用 entries 实现 Map 的遍历(最常见和最常用的)。
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("Java入门教程", "http://c.biancheng.net/java/");
map.put("C语言入门教程", "http://c.biancheng.net/c/");
for (Map.Entry<String, String> entry : map.entrySet()) {
String mapKey = entry.getKey();
String mapValue = entry.getValue();
System.out.println(mapKey + ":" + mapValue);
}
}
2)使用 for-each 循环遍历 key 或者 values,一般适用于只需要 Map 中的 key 或者 value 时使用。性能上比 entrySet 较好。
Map<String, String> map = new HashMap<String, String>();
map.put("Java入门教程", "http://c.biancheng.net/java/");
map.put("C语言入门教程", "http://c.biancheng.net/c/");
// 打印键集合
for (String key : map.keySet()) {
System.out.println(key);
}
// 打印值集合
for (String value : map.values()) {
System.out.println(value);
}
3)使用迭代器(Iterator)遍历
Map<String, String> map = new HashMap<String, String>();
map.put("Java入门教程", "http://c.biancheng.net/java/");
map.put("C语言入门教程", "http://c.biancheng.net/c/");
Iterator<Entry<String, String>> entries = map.entrySet().iterator();
while (entries.hasNext()) {
Entry<String, String> entry = entries.next();
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key + ":" + value);
}
4)通过键找值遍历,这种方式的效率比较低,因为本身从键取值是耗时的操作,不推荐使用。
for(String key : map.keySet()){
String value = map.get(key);
System.out.println(key+":"+value);
}
Map实现HashMap
HashMap是Java中最常用K-V容器,采用了哈希的方式进行实现,HashMap中存储的是一个又一个Key-Value的键值对,我们将其称作Entry,HashMap对Entry进行了扩展(称作Node),使其成为链表或者树的结构使其存储在HashMap的容器里(是一个数组)。
Map实现LinkedHashMap
Map实现TreeMap
- TreeSet/TreeMap 是⾃平衡⼆叉树
- TreeSet/TreeMap 迭代器采⽤的是中序遍历⽅式
- ⽆序,不可重复,但是可排序。
排序规则定义与TreeSet类似。
Stream流
为了简化集合和数组操作的API,JDK8提出了Stream用于操作集合和数组中的元素,结合lambda。
Stream流与C++中的算法思路非常相像。
Java 8 还新增了 Stream、IntStream、LongStream、DoubleStream 等流式 API,这些 API 代表多个支持串行和并行聚集操作的元素。上面 4 个接口中,Stream 是一个通用的流接口,而 IntStream、LongStream、 DoubleStream 则代表元素类型为 int、long、double 的流。
Java 8 还为上面每个流式 API 提供了对应的 Builder,例如 Stream.Builder、IntStream.Builder、LongStream.Builder、DoubleStream.Builder,开发者可以通过这些 Builder 来创建对应的流。
流除了流式编程这一特点外,还有一个特点就是:只能使用一次!!!
流使用一次后就自动关闭流了
获取stream流的方式
1、由集合的.stream()获取stream
2、由数组的Arrays.stream(),获取stream
3、由stream的 of() 方法将数组转为stream
// 1、由集合的.stream()获取stream
List<String> l1 = new ArrayList<>();
Collections.addAll(l1, "daf", "fdaf", "fdaf");
Stream<String> l1_s = l1.stream(); // 通过集合自身的方法获取
// 2、由数组的Arrays.stream(),获取stream
String[] names = {"fdafd", "fdafd", "fadx"};
Stream<String> l1_array = Arrays.stream(names);
// 3、由stream的 of() 方法将数组转为stream
Stream<String> l1_stream = Stream.of(names);
常用API
常用筛选方法:
常用末端方法:
使用案例:
filter过滤 + count统计
filter过滤 + findFirst返回满足条件的一个
map隐射
map加工为新的元素
concat合并流
collect收集流:再把流转到集合中去
Set<String> objs = new HashSet<>();
Collections.addAll(objs, "水果:苹果", "水果:李子", "蔬菜:白菜","水果:香蕉", "蔬菜:胡萝卜");
System.out.println("水果种类:" + objs.stream().filter(ele -> ele.contains("水果")).count());
System.out.println("名字最长:" + objs.stream().filter(ele -> ele.length() > 5).findFirst());
System.out.println("水果的名字长度:");
objs.stream().mapToInt(ele -> ele.substring(3).length()).forEach(System.out::println);
Collections.addAll(objs2, "J♦", "8♥" ,"5♠");
objs2.stream().map(ele -> new Card(ele.substring(0, 1), ele.substring(1), 0)).forEach(System.out::println);
System.out.println("------------合并流--------");
Stream<String> objs3 = Stream.concat(objs.stream(), objs2.stream());
objs3.forEach(System.out::println);
异常
异常是程序在“编译”或者执行过程中可能出现的问题,比如数组索引越界、空指针异常、日期格式化异常等。
异常一旦出现,如果没有提取处理,程序就会退出JVM虚拟机而终止。
提前处理异常体现的是程序的安全性。
编译时异常
一般而言,第三种,异常抛出给调用者,调用者捕获处理是最佳方式
try……catch……
public static void parseTime(String data) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
Date d = sdf.parse(data);
System.out.println(d);
InputStream is = new FileInputStream("E:/meiinv.jpg");
} catch (Exception e) {
e.printStackTrace();
}
}
运行时异常
自定义异常
流对象
java流
在 Java 中所有数据都是使用流读写的。流是一组有序的数据序列,将数据从一个地方带到另一个地方。根据数据流向的不同,可以分为输入(Input)流和输出(Output)流两种。
输入流
Java 流相关的类都封装在 java.io 包中,而且每个数据流都是一个对象。所有输入流类都是 InputStream 抽象类(字节输入流)和 Reader 抽象类(字符输入流)的子类。其中 InputStream 类是字节输入流的抽象类,是所有字节输入流的父类,其层次结构如图 3 所示。
InputStream 类中所有方法遇到错误时都会引发 IOException 异常。如下是该类中包含的常用方法。
Java 中的字符是 Unicode 编码,即双字节的
,而 InputerStream 是用来处理单字节的,在处理字符文本时不是很方便。这时可以使用 Java 的文本输入流 Reader 类
,该类是字符输入流
的抽象类,即所有字符输入流的实现都是它的子类,该类的方法与 InputerSteam 类的方法类似。
输出流
在 Java 中所有输出流类都是 OutputStream 抽象类(字节输出流)和 Writer 抽象类(字符输出流)的子类。其中 OutputStream 类是字节输出流
的抽象类,是所有字节输出流的父类,其层次结构如图 4 所示。
OutputStream 类是所有字节输出流的超类,用于以二进制的形式将数据写入目标设备,该类是抽象类,不能被实例化。OutputStream 类提供了一系列跟数据输出有关的方法,如下所示。
File类常用API
在 Java 中,File 类是 java.io 包中唯一代表磁盘文件本身的对象,也就是说,如果希望在程序中操作文件和目录,则都可以通过 File 类来完成。File 类定义了一些方法来操作文件,如新建、删除、重命名文件和目录等。
File 类不能访问文件内容本身,如果需要访问文件内容本身,则需要使用输入/输出流。
File 类提供了如下三种形式构造方法。
- File(String path):如果 path 是实际存在的路径,则该 File 对象表示的是目录;如果 path 是文件名,则该 File 对象表示的是文件。
- File(String path, String name):path 是路径名,name 是文件名。
- File(File dir, String name):dir 是路径对象,name 是文件名。
使用任意一个构造方法都可以创建一个 File 对象,然后调用其提供的方法对文件进行操作。在表 1 中列出了 File 类的常用方法及说明。
创建和删除文件
File 类不仅可以获取已知文件的属性信息,还可以在指定路径创建文件,以及删除一个文件。创建文件需要调用 createNewFile() 方法,删除文件需要调用 delete() 方法。无论是创建还是删除文件通常都先调用 exists() 方法判断文件是否存在。
在操作文件时一定要用File.separator
表示分隔符,因为不同操作系统文件分隔符不一致,Windows 中使用反斜杠\表示目录的分隔符。Linux 中使用正斜杠/表示目录的分隔符。
static void fileDetect() throws IOException {
String path = "C" + File.separator + "test.txt";
File f = new File(path);
if (f.exists()) {
f.delete();
}
f.createNewFile();
}
遍历目录
listFiles()
通过遍历目录可以在指定的目录中查找文件,或者显示所有的文件列表。File 类的 list() 方法提供了遍历目录功能,该方法有如下两种重载形式。
- String[] list()
该方法表示返回由 File 对象表示目录中所有文件和子目录名称组成的字符串数组,如果调用的 File 对象不是目录,则返回 null。 - String[] list(FilenameFilter filter)
该方法的作用与 list() 方法相同,不同的是返回数组中仅包含符合 filter 过滤器的文件和目录,如果 filter 为 null,则接受所有名称。
假设要遍历 C 盘根目录下的所有文件和目录,并显示文件或目录名称、类型及大小。使用 list() 方法的实现代码如下:
注意:assert fileList != null;
java中如果对象为空是不能使用加强for循环遍历的。
static void fileTraverse() throws IOException {
String path = "C:\\Users\\Administrator\\Desktop\\找工作\\简历更新\\";
File f = new File(path);
System.out.println("文件名称\t\t文件类型\t\t文件大小");
String[] fileList = f.list();
assert fileList != null;
for (String s : fileList) {
System.out.println(s + "\t\t");
System.out.print((new File(path, s)).isFile() ? "文件" + "\t\t" : "文件夹" + "\t\t");
System.out.println((new File(path, s)).length() + "字节");
}
}
搜索文件
模糊搜索文件名name。
public static void searchFile(File dir, String name) {
if (dir != null && dir.isDirectory()) {
File[] files = dir.listFiles();
if (files != null && files.length > 0) {
for (File file : files) {
if (file.isFile()) {
if (file.getName().contains(name)) {
System.out.println("find:" + file.getAbsolutePath());
}
} else {
searchFile(file, name);
}
}
}
} else {
System.out.println("not dir");
}
}
字符集
计算机中,任何的文字都是以指定的编码方式存在的,在 Java 程序的开发中最常见的是 ISO8859-1、GBK/GB2312、Unicode、 UTF 编码。
Java 中常见编码说明如下:
- ISO8859-1:属于单字节编码,最多只能表示 0~255 的字符范围。
- GBK/GB2312:中文的国标编码,用来表示汉字,属于双字节编码。GBK 可以表示简体中文和繁体中文,而 GB2312 只能表示简体中文。GBK 兼容 GB2312。
- Unicode:是一种编码规范,是为解决全球字符通用编码而设计的。UTF-8 和 UTF-16 是这种规范的一种实现,此编码不兼容 ISO8859-1 编码。Java 内部采用此编码。
- UTF:UTF 编码兼容了 ISO8859-1 编码,同时也可以用来表示所有的语言字符,不过 UTF 编码是不定长编码,每一个字符的长度为 1~6 个字节不等。一般在中文网页中使用此编码,可以节省空间。
在程序中如果处理不好字符编码,就有可能出现乱码问题。例如现在本机的默认编码是 GBK,但在程序中使用了 ISO8859-1 编码,则就会出现字符的乱码问题。就像两个人交谈,一个人说中文,另外一个人说英语,语言不同就无法沟通。为了避免产生乱码,程序编码应与本地的默认编码保持一致。
IO流
IO流抽象接口
FileInputStream
static void fileTraverse() throws IOException {
String path = "C:\\Users\\Administrator\\Desktop\\找工作\\简历更新\\data.txt";
InputStream in = new FileInputStream(path);
// int b;
// while ((b = in.read()) != -1) {
// System.out.print((char) b);
// }
byte[] buffer = new byte[3];
int len;
while ((len = in.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, len));
}
}
FileOutPutStream
FileOutputStream 类继承自 OutputStream 类,重写和实现了父类中的所有方法。FileOutputStream 类的对象表示一个文件字节输出流,可以向流中写入一个字节或一批字节。在创建 FileOutputStream 类的对象时,如果指定的文件不存在,则创建一个新文件;如果文件已存在,则清除原文件的内容重新写入。
注意:
1、getBytes(),在idea上默认获取的是UTF-8的字节数组,如果想用其他字符集,可以使用参数指定,例如:getBytes(“GBK”);
2、flush()刷新数据的作用,是强制将缓存中的输出流(字节流,字符流等)强制输出,并清空缓冲区
,因为输出流向某个文件写入内容时,先将输出流写入到缓冲区,当缓冲区满了后,才将缓冲区内容输出到file,flush之后,输出流还是可以使用的。
3、colse()方法是关系流,close操作是包含flush()刷新操作的,关闭流后,就不能再使用了。
文件拷贝案例
static void fileTraverse() throws IOException {
String path = "C:\\Users\\Administrator\\Desktop\\找工作\\简历更新\\data.txt";
String target = path.subSequence(0, path.length() - 4) + "副本" + path.substring(path.length() - 4);
try {
InputStream is = new FileInputStream(path);
OutputStream os = new FileOutputStream(target);
byte[] buffer = new byte[2];
int len; // 记录每次读取的字节数
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
System.out.println("finished");
os.close();
is.close();
} catch (Exception e) {
e.printStackTrace();
}
}
FileReader
字符流适合读取文本文件,其方法与字节流是基本一致的。
FileWriter
资源释放
try-catch-finally
- finally:在异常处理时提供finally块来执行所有清除操作,比如IO流中的释放资源
- 特点:
被finally控制的语句最终一定会执行,除非JVM退出
在上边的案例中,资源关闭放在在try中,如果中间出现异常,就会导致最后的关闭流的操作没有执行,那么资源就无法正常释放。
static void fileTraverse() throws IOException {
String path = "C:\\Users\\Administrator\\Desktop\\找工作\\简历更新\\data.txt";
String target = path.subSequence(0, path.length() - 4) + "副本" + path.substring(path.length() - 4);
InputStream is = new FileInputStream(path);
OutputStream os = new FileOutputStream(target);
try {
byte[] buffer = new byte[2];
int len; // 记录每次读取的字节数
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
System.out.println("finished");
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("=====finally======");
try {
os.close();
} catch (Exception e) {
e.printStackTrace();
}
try {
is.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
try-with-resource
JDK7和JKD9简化了资源释放操作,在try的同时定义流,那么在catch到异常时会自动关闭流。
JDK7做法:在try的括号内定义资源对象。
static void fileTraverse() throws IOException {
String path = "C:\\Users\\Administrator\\Desktop\\找工作\\简历更新\\data.txt";
String target = path.subSequence(0, path.length() - 4) + "副本" + path.substring(path.length() - 4);
try(
InputStream is = new FileInputStream(path);
OutputStream os = new FileOutputStream(target);
) {
byte[] buffer = new byte[2];
int len; // 记录每次读取的字节数
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
System.out.println("finished");
} catch (Exception e) {
e.printStackTrace();
}
}
JDK9做法:在外边定义流资源,在try括号内捕获资源,以分号;
分隔。
String path = "C:\\Users\\Administrator\\Desktop\\找工作\\简历更新\\data.txt";
String target = path.subSequence(0, path.length() - 4) + "副本" + path.substring(path.length() - 4);
InputStream is = new FileInputStream(path);
OutputStream os = new FileOutputStream(target);
try(is ; os) {
byte[] buffer = new byte[2];
int len; // 记录每次读取的字节数
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
System.out.println("finished");
} catch (Exception e) {
e.printStackTrace();
}
}
缓冲流
缓冲流自带8KB的缓冲区,可以提高原始字节流、字符流读写数据的性能。
static void fileTraverse() throws IOException {
String path = "C:\\Users\\Administrator\\Desktop\\找工作\\简历更新\\figure.jpg";
String target = path.subSequence(0, path.length() - 4) + "副本" + path.substring(path.length() - 4);
try (
InputStream is = new FileInputStream(path);
OutputStream os = new FileOutputStream(target);
InputStream bis = new BufferedInputStream(is);
OutputStream bos = new BufferedOutputStream(os);
) {
byte[] buffer = new byte[8096];
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
System.out.println("ok");
} catch (Exception e) {
e.printStackTrace();
}
}
转换流
正常情况下,字节流可以对所有的数据进行操作,但是有些时候在处理一些文本时我们要用到字符流,比如,查看文本的中文时就是需要采用字符流更为方便。所以 Java IO 流中提供了两种用于将字节流转换为字符流的转换流。
InputStreamReader 用于将字节输入流转换为字符输入流,其中 OutputStreamWriter 用于将字节输出流转换为字符输出流。使用转换流可以在一定程度上避免乱码,还可以指定输入输出所使用的字符集。
转换流属于字符流
例子1:读取GBK编码的文本文件,将其重写写为utf-8编码。
1、InputStream is = new FileInputStream(path);
2、InputStreamReader ir = new InputStreamReader(is, “GBK”); // 转换流,以GBK的方法读取输入流
3、用缓冲流接收转换流
BufferedReader br = new BufferedReader(ir);
4、写入。
static void fileTraverse() throws IOException {
String path = "C:\\Users\\Administrator\\Desktop\\找工作\\简历更新\\data_gbk.txt";
String target = path.subSequence(0, path.length() - 4) + "副本" + path.substring(path.length() - 4);
try (
InputStream is = new FileInputStream(path);
InputStreamReader ir = new InputStreamReader(is, "GBK");
Writer w = new FileWriter(target);
BufferedReader br = new BufferedReader(ir);
){
String line;
while ((line = br.readLine()) != null) {
w.write(line);
}
System.out.println("ok");
} catch (IOException e) {
e.printStackTrace();
}
}
例子2:下面以获取键盘输入为例来介绍转换流的用法。Java 使用 System.in 代表标准输出,即键盘输入,但这个标准输入流是 InputStream 类的实例,使用不太方便,而且键盘输入内容都是文本内容,所以可以使用 InputStreamReader 将其转换成字符输入流,普通的 Reader 读取输入内容时依然不太方便,可以将普通的 Reader 再次包装成 BufferedReader,利用 BufferedReader 的 readLine() 方法可以一次读取一行内容。
// 1、输入字符流接收屏幕打印输入
try {
InputStreamReader reader = new InputStreamReader(System.in);
// 2、使用缓冲流提高转换速度
BufferedReader br = new BufferedReader(reader);
String line = null;
while ((line = br.readLine()) != null) {
// 如果读取到exit,退出
if (line.equals("exit")) {
System.exit(1);
}
System.out.println("输入内容为:" + line);
}
} catch (IOException e) {
e.printStackTrace();
}
对象序列化和反序列化
“序列化”是一种把对象的状态转化成字节流的机制,“反序列”是其相反的过程,把序列化成的字节流用来在内存中重新创建一个实际的Java对象。这个机制被用来==“持久化”==对象。
对象的序列化是与平台无关的,因此在一个平台上被“序列化”的对象可以很容易的在另一个不相同的平台上给“反序列化”出来。
序列化ObjectOutputStream流
为了使一个Java对象能够被“序列化”,我们必须让这个对象实现java.io.Serializable接口。
打开Serializable.java这个文件,发现它没有定义任何内容,因此它是一个标识接口
,标识对象具备序列化能力。
package test1;
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
上面就是一个实现了Serializable接口的Person类对象,这个对象现在就已经具备了“可序列化”的能力。此类的对象是可以经过“二进制数据流”进行传输的。而如果想要完成对象的输入或者输出,还必须依靠对象输出流(ObjectOutputStream)和对象输入流(ObjectInputStream)。
接下来使用“对象输出流”来序列化一个Java对象:
public static void SerializableWrite() {
String path = "C:\\Users\\Administrator\\Desktop\\找工作\\简历更新\\serialize.txt";
Person p = new Person("陈昆", 38);
try (
OutputStream os = new FileOutputStream(path);
ObjectOutputStream oos = new ObjectOutputStream(os);
){
oos.writeObject(p);
System.out.println("序列化完成");
} catch (Exception e){
e.printStackTrace();
}
}
如果打开保存的序列化文件serialize.txt,会发现乱码,因为保存的内容都是二进制的。
需要注意的是,对象的“序列化”并没有保存对象所有的内容,而仅仅保留了对象的属性,没有保留对象的方法
,之所以这么做的原因是同一类对象中每个对象都具备相同的方法,但是每个对象的属性却不一定相同,所以我们序列化时只要保存对象的属性就可以了。
反序列化ObjectInputStream流
使用ObjectInputStream可以直接把被序列化的对象给反序列化出来。
下面使用ObjectInputStream流把我们刚刚序列化的Person对象给反序列化出来,使其成为一个实际的Java对象。
public static void SerializableRead() {
String path = "C:\\Users\\Administrator\\Desktop\\找工作\\简历更新\\serialize.txt";
Person p = new Person("陈昆", 38);
try (
InputStream is = new FileInputStream(path);
ObjectInputStream oos = new ObjectInputStream(is);
){
Person P = (Person) oos.readObject();
System.out.println("反序列化完成");
System.out.println(p);
} catch (Exception e){
e.printStackTrace();
}
}
控制台打印结果为
总结:对象输出流接收文件输出流作为参数构造,使用writeObject()
序列化对象
对象输入流接收文件输入流作为参数构造,使用readObject()
反序列化对象
OutputStream os = new FileOutputStream(path);
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(p);
InputStream is = new FileInputStream(path);
ObjectInputStream oos = new ObjectInputStream(is);
Person P = (Person) oos.readObject();
对象序列化和反序列化的版本兼容问题
注意:使用关键字transient
修饰的属性,不参与序列化的过程。
有时我们会修改对象的属性,因此会使用serialVersionUID ,来标记序列化对象的版本号,如果反序列化时版本号冲突会出错。
public class Person implements Serializable {
private String name;
private int age;
private transient String pws;
private static final long serialVersionUID = 1;
}
在对象进行序列化或者反序列化的操作时,要考虑JDK的版本问题。如果序列化时用的Class版本与反序列化时用的Class版本不一致的话就有可能造成异常。所以在序列化中引入了一个serialVersionUID
的常量。
可以通过这个常量来验证序列化版本的一致性。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地的相应的serialVersionUID进行一个比较,如果相同就认为是一致的,可以进行反序列化的操作,否则就会发生因为序列化版本不一致的异常。
序列化的注意点:
- 如果父类已经实现了Serializable序列化接口的话,其子类就不用再实现这个接口了,但是反过来就不成立了。
- 只有非静态的数据成员才会通过序列化操作被序列化。
- 使用
transient
修饰的非静态数据成员不会被序列化。 - 当一个对象被反序列化时,这个对象的构造函数不会再被调用。
- 需要序列化的对象,如果内部引用了一个没有实现序列化接口的对象,则无法序列化。
序列化文件的几种格式
- XML:把对象序列化为XML格式的文件,然后就可以通过网络传输这个对象或者把它储存进文件或数据库里,可以使用
JAXB库
- JSON:同样可以把对象序列化为JSON格式而持久化保存对象,可以使用
GSON库
实现。