赞
踩
在看这篇文章之前先预习java基础
这部分知识一共有4个文档
第三个是当前这个文档
关于这部分的源码如下
javase从入门到精通的学习代码.rar
讲述集合的时候,如果对python感兴趣
也可以对比一下两者的不同
python数据类型详细分析(附代码)
list.add(100); //自动装箱Integer
一类是单个方式存储元素:单个方式存储元素,这一类集合中超级父接口:java.util.Collection;
Collection接口继承Iterable接口(此接口有Iterator方法)
调用此方法得到迭代器对象,执行方法下的三个函数 hasNext,next和remove
list和set集合继承了collection接口,接口是不可以new的
一类是以键值对儿的方式存储元素:以键值对的方式存储元素,这一类集合中超级父接口:java.util.Map;
Map集合和Collection集合没有关系,存储的方式不同
所有Map集合key元素无序不可重复
Map接口的常用实现类有
总结:
所有的实现类:
实现类 | 底层元素 |
---|---|
Arraylist | 底层是数组。 |
LinkedList | 底层是双向链表。 |
Vector | 底层是数组,线程安全的,效率较低,使用较少。 |
HashSet | 底层是HashMap,放到 HashSet集合中的元素等同于放到HashMap集合key部分了。 |
TreeSet | 底层是TreeMap,放到 TreeSet集合中的元素等同于放到TreeMap集合key部分了。 |
HashMap | 底层是哈希表。 |
Hashtable | 底层也是哈希表,只不过线程安全的,效率较低,使用较少。 |
Properties | 是线程安全的,并且 key 和value只能存储字符串String。 |
TreeMap | 底层是二叉树。TreeMap集合的 key可以自动按照大小顺序排序。 |
集合的总体概念如下:
- set集合存储元素的特点:
无序不可重复
无序:存进去的顺序和取出的顺序不一定相同。另外 set集合中元素没有下标。
不可重复:存进去1,不能再存储1了。
- SortedSet ( SortedMap)集合存储元素特点:
无序不可重复的,但是SortedSet集合中的元素是可排序的。
无序:存进去的顺序和取出的顺序不一定相同。另外 set集合中元素没有下标。
不可重复:存进去1,不能再存储1
可排序:可以按照大小顺序排列。
- Map集合的 key,就是一个set集合。
往set集合中放数据,实际上放到了Map集合的 key 部分。
集合中不能直接存储基本数据类型,也不能存java对象,只是存储java对象的内存地址。
collection是一个接口
常用方法有:
函数 | 功能 |
---|---|
boolean add(Object e) | 向集合中添加元素 |
int size() | 获取集合中元素的个数 |
void clear() | 清空集合 |
boolean contains(Object o) | 判断当前集合中是否包含元素o,包含返回true,不包含返回false |
boolean remove(Object o) | 删除集合中的某个元素。 |
boolean isEmpty() | 判断该集合中元素的个数是否为0 |
Object[] toArray() | 调用这个方法可以把集合转换成数组。【作为了解,使用不多。】 |
下面举例常用方法的注意事项
add方法中可以添加常用的数据,也可以new对象
Collection c = new ArrayList();
c.add(1200); // 自动装箱(java5的新特性。),实际上是放进去了一个对象的内存地址。Integer x = new Integer(1200);
c.add(3.14); // 自动装箱
c.add(new Object());
c.add(new Student());
c.add(true); // 自动装箱
c.add("hello"); // "hello"对象的内存地址放到了集合当中
size()返回的是元素的个数
clear可以清空所有元素
contains返回的是boolean类型
讲解collection的集合迭代,而且迭代器是通用的
这个迭代器是Collection的上层接口Iterator,而且迭代器一开始没有指向第一个元素
迭代器是对象,迭代器常用的两个方法为
具体步骤为:
最开始先获取集合对象Collection c = new ArrayList();
这个对象有iterator()
方法。
通过集合对象调用迭代器的接口(接口方法返回接口)Iterator it = c.iterator();
在调用迭代器的方法
这个只能返回object,Object obj = it.next();
而且在输出的时候输出的是obj的内容,因为重写了tosting方法
while(it.hasNext()){
Object obj = it.next();
System.out.println(obj);
}
注意:此时获取的迭代器,指向的是那是集合中没有元素状态下的迭代器。
一定要注意:集合结构只要发生改变,迭代器必须重新获取。
当集合结构发生了改变,迭代器没有重新获取时,调用next()方法时:java.util.ConcurrentModificationException
不可以这样子
// 获取迭代器
//Iterator it = c.iterator();
/*while(it.hasNext()){
// 编写代码时next()方法返回值类型必须是Object。
// Integer i = it.next();
Object obj = it.next();
System.out.println(obj);
}*/
关于集合中迭代的代码展示
/* 关于集合的迭代/遍历 */ public class CollectionTest03 { public static void main(String[] args) { // 创建集合对象 Collection c1 = new ArrayList(); // ArrayList集合:有序可重复 // 添加元素 c1.add(1); c1.add(2); c1.add(3); c1.add(4); c1.add(1); // 迭代集合 Iterator it = c1.iterator(); while(it.hasNext()){ // 存进去是什么类型,取出来还是什么类型。 Object obj = it.next(); /*if(obj instanceof Integer){ System.out.println("Integer类型"); }*/ // 只不过在输出的时候会转换成字符串。因为这里println会调用toString()方法。 System.out.println(obj); } // HashSet集合:无序不可重复 Collection c2 = new HashSet(); // 无序:存进去和取出的顺序不一定相同。 // 不可重复:存储100,不能再存储100. c2.add(100); c2.add(200); c2.add(300); c2.add(90); c2.add(400); c2.add(50); c2.add(60); c2.add(100); Iterator it2 = c2.iterator(); while(it2.hasNext()){ System.out.println(it2.next()); } } }
hashset无序不可重复,而ArrayList有序可重复
不可重复,就是不能重复add进相同的数字
深入contains方法
判断集合中是否包含某个元素
比较的是内容,是因为内部中调用了equals方法进行比对
String类的equals方法比较的是内容不是内存地址
object类的equals比较的是地址
主要是因为string类的equals重写了,比较的是内容
可以通过代码进行比较
代码展示
public class CollectionTest04 { public static void main(String[] args) { // 创建集合对象 Collection c = new ArrayList(); // 向集合中存储元素 String s1 = new String("abc"); // s1 = 0x1111 c.add(s1); // 放进去了一个"abc" String s2 = new String("def"); // s2 = 0x2222 c.add(s2); // 集合中元素的个数 System.out.println("元素的个数是:" + c.size()); // 2 // 新建的对象String String x = new String("abc"); // x = 0x5555 // c集合中是否包含x?结果猜测一下是true还是false?、 System.out.println(c.contains(x)); //判断集合中是否存在"abc" true,结果为true就有 } }
contains主要是因为重写equals方法,所以比较的是内容
查看源码也可看出重写了equals方法
而且通过自个定义函数重写
public class CollectionTest05 { public static void main(String[] args) { // 创建集合对象 Collection c = new ArrayList(); // 创建用户对象 User u1 = new User("jack"); // 加入集合 c.add(u1); // 判断集合中是否包含u2 User u2 = new User("jack"); // 没有重写equals之前:这个结果是false //System.out.println(c.contains(u2)); // false // 重写equals方法之后,比较的时候会比较name。 System.out.println(c.contains(u2)); // true } } class User{ private String name; public User(){} public User(String name){ this.name = name; } // 重写equals方法 // 将来调用equals方法的时候,一定是调用这个重写的equals方法。 // 这个equals方法的比较原理是:只要姓名一样就表示同一个用户。 public boolean equals(Object o) { if(o == null || !(o instanceof User)) return false; if(o == this) return true; User u = (User)o; // 如果名字一样表示同一个人。(不再比较对象的内存地址了。比较内容。) return u.name.equals(this.name); } }
remove方法也是重写了equals方法
如果删除某个元素,这个元素有两个一摸一样的
删除这个,这两个元素都会删除
// 创建集合对象
Collection cc = new ArrayList();
// 创建字符串对象
String s1 = new String("hello");
// 加进去。
cc.add(s1);
// 创建了一个新的字符串对象
String s2 = new String("hello");
// 删除s2
cc.remove(s2); // s1.equals(s2) java认为s1和s2是一样的。删除s2就是删除s1。
// 集合中元素个数是?
System.out.println(cc.size()); // 0
前面也讲过迭代器中途不可以改变一点变化
如果改变了会出错
比如下面的代码展示
Collection c2 = new ArrayList(); c2.add("abc"); c2.add("def"); c2.add("xyz"); Iterator it2 = c2.iterator(); while(it2.hasNext()){ Object o = it2.next(); // 删除元素 // 删除元素之后,集合的结构发生了变化,应该重新去获取迭代器 // 但是,循环下一次的时候并没有重新获取迭代器,所以会出现异常:java.util.ConcurrentModificationException // 出异常根本原因是:集合中元素删除了,但是没有更新迭代器(迭代器不知道集合变化了) //c2.remove(o); // 直接通过集合去删除元素,没有通知迭代器。(导致迭代器的快照和原集合状态不同。) // 使用迭代器来删除可以吗? // 迭代器去删除时,会自动更新迭代器,并且更新集合(删除集合中的元素)。 it2.remove(); // 删除的一定是迭代器指向的当前元素。 System.out.println(o); } System.out.println(c2.size()); //0
主要是集合中删除的方法只是集合快照,如果有更新集合,会出现错误,如果使用的是迭代器的删除方法,如果有更新集合,集合也会随之更新而不影响删除的同步
List集合存储元素特点:有序可重复。有序:List集合中的元素有下标。从0开始,以1递增。重复:存储一个1,还可以再存储1.
List是Collection接口的子接口
以下只列出List接口特有的常用的方法:
常用方法 | 功能说明 |
---|---|
void add(int index, Object element) | 某个索引添加元素 |
Object set(int index, Object element) | 某个索引设置元素 |
Object get(int index) | 获取该索引的元素 |
int indexOf(Object o) | 获取对象第一次出现的索引 |
int lastIndexOf(Object o) | 获取对象最后一次出现的索引 |
Object remove(int index) | 删除指定下标的元素 |
// 创建List类型的集合。 //List myList = new LinkedList(); //List myList = new Vector(); List myList = new ArrayList(); // 添加元素 myList.add("A"); // 默认都是向集合末尾添加元素。 myList.add("B"); myList.add("C"); myList.add("C"); myList.add("D"); //在列表的指定位置插入指定元素(第一个参数是下标) // 这个方法使用不多,因为对于ArrayList集合来说效率比较低。 myList.add(1, "KING"); // 迭代 Iterator it = myList.iterator(); while(it.hasNext()){ Object elt = it.next(); System.out.println(elt); }
具体遍历元素也可以通过下标值
// 因为有下标,所以List集合有自己比较特殊的遍历方式
// 通过下标遍历。【List集合特有的方式,Set没有。】
for(int i = 0; i < myList.size(); i++){
Object obj = myList.get(i);
System.out.println(obj);
}
其他方法比较简单,就不展示出代码
默认初始化容量10(底层先创建了一个长度为0的数组,当添加第一个元素的时候,初始化容量10。)
集合底层是一个Object[]数组。
构造方法:
new ArrayList();
new ArrayList(20);
ArrayList集合的扩容:增长到原容量的1.5倍。
ArrayList集合底层是数组,尽可能少的扩容。因为数组扩容效率比较低,建议在使用ArrayList集合的时候预估计元素的个数,给定一个初始化容量
数组优点:检索效率比较高。(每个元素占用空间大小相同,内存地址是连续的,通过数学表达式计算出元素的内存地址,检索效率高。)
数组缺点:随机增删元素效率比较低。另外数组无法存储大数据量。(很难找到一块非常巨大的连续的内存空间。)
向数组末尾添加元素,效率很高,不受影响。
面试官经常问的一个问题?
这么多的集合中,你用哪个集合最多?
答:ArrayList集合。
因为往数组末尾添加元素,效率不受影响。
另外,我们检索/查找某个元素的操作比较多。
ArrayList集合是非线程安全的。(不是线程安全的集合。)
基本的代码实现
注意区分容量与数组的空间大小size
// 默认初始化容量是10
// 数组的长度是10
List list1 = new ArrayList();
// 集合的size()方法是获取当前集合中元素的个数。不是获取集合的容量。
System.out.println(list1.size()); // 0
// 指定初始化容量
// 数组的长度是20
List list2 = new ArrayList(20);
// 集合的size()方法是获取当前集合中元素的个数。不是获取集合的容量。
System.out.println(list2.size()); // 0
构造方法除了以上两种之外 还有第三种转换方式
将HashSet集合转换成List集合
// 默认初始化容量10 List myList1 = new ArrayList(); // 指定初始化容量100 List myList2 = new ArrayList(100); // 创建一个HashSet集合 Collection c = new HashSet(); // 添加元素到Set集合 c.add(100); c.add(200); c.add(900); c.add(50); // 通过这个构造方法就可以将HashSet集合转换成List集合。 List myList3 = new ArrayList(c); for(int i = 0; i < myList3.size(); i++){ System.out.println(myList3.get(i)); }
再讲这部分知识的时候,涉及单链表和双向链表以及对比线性表的区别
可查看我之前写过的文章
【数据结构】顺序表及链表详细分析(全)
具体涉及单链表和双链表的创建可查看这篇代码
【leetcode】链表-设计链表(单双链表全)
具体单链表的创建和功能
简单阐述基本功能
创建链表
public class Link { public static void main(String[] args) { // 头节点 Node header; int size = 0; public int size(){ return size; } // 向链表中添加元素的方法(向末尾添加) public void add(E data){ //public void add(Object data){ // 创建一个新的节点对象 // 让之前单链表的末尾节点next指向新节点对象。 // 有可能这个元素是第一个,也可能是第二个,也可能是第三个。 if(header == null){ // 说明还没有节点。 // new一个新的节点对象,作为头节点对象。 // 这个时候的头节点既是一个头节点,又是一个末尾节点。 header = new Node(data, null); }else { // 说明头不是空! // 头节点已经存在了! // 找出当前末尾节点,让当前末尾节点的next是新节点。 Node currentLastNode = findLast(header); currentLastNode.next = new Node(data, null); } size++; } /** * 专门查找末尾节点的方法。 */ private Node findLast(Node node) { if(node.next == null) { // 如果一个节点的next是null // 说明这个节点就是末尾节点。 return node; } // 程序能够到这里说明:node不是末尾节点。 return findLast(node.next); // 递归算法! } }
创建单链表功能表述
/* 单链表中的节点。 节点是单向链表中基本的单元。 每一个节点Node都有两个属性: 一个属性:是存储的数据。 另一个属性:是下一个节点的内存地址。 */ public class Node { // 存储的数据 Object data; // 下一个节点的内存地址 Node next; public Node(){ } public Node(Object data, Node next){ this.data = data; this.next = next; } }
通过以上代码可看出链表的优缺点:
链表的优点:
由于链表上的元素在空间存储上内存地址不连续。
所以随机增删元素的时候不会有大量元素位移,因此随机增删效率较高。
在以后的开发中,如果遇到随机增删集合中元素的业务比较多时,建议使用LinkedList。
链表的缺点:
不能通过数学表达式计算被查找元素的内存地址,每一次查找都是从头节点开始遍历,直到找到为止。所以LinkedList集合检索/查找的效率较低。
LinkedList集合底层也是有下标的。
注意:ArrayList之所以检索效率比较高,不是单纯因为下标的原因。是因为底层数组发挥的作用。
LinkedList集合照样有下标,但是检索/查找某个元素的时候效率比较低,因为只能从头节点开始一个一个遍历。
List list = new LinkedList();
list.add("a");
list.add("b");
list.add("c");
for(int i = 0; i <list.size(); i++){
Object obj = list.get(i);
System.out.println(obj);
}
List list2 = new ArrayList(); // 这样写表示底层用了数组
List list2 = new LinkedList(); // 这样写表示底层用了双向链表
创建一个集合
// 创建一个Vector集合
List vector = new Vector();
//Vector vector = new Vector();
遍历集合数组
Iterator it = vector.iterator();
while(it.hasNext()){
Object obj = it.next();
System.out.println(obj);
}
使用非线程安全转换为线程安全
这里区别一下:
java.util.Collection 是集合接口。
java.util.Collections 是集合工具类。
// 这个可能以后要使用!!!!
List myList = new ArrayList(); // 非线程安全的。
// 变成线程安全的
Collections.synchronizedList(myList); // 这里没有办法看效果,因为多线程没学,你记住先!
// myList集合就是线程安全的了。
myList.add("111");
myList.add("222");
myList.add("333");
只在程序编译阶段起作用,只是给编译器参考的。(运行阶段泛型没用!)
泛型优点:
缺点:
在这之前必须掌握几个点
obj instanceof Animal
,本身所有类都有继承object的具体可看我这篇文章
java之instanceof用法详细分析(全)
public class GenericTest01 { public static void main(String[] args) { List myList = new ArrayList(); // 准备对象 Cat c = new Cat(); Bird b = new Bird(); // 将对象添加到集合当中 myList.add(c); myList.add(b); // 遍历集合,取出每个Animal,让它move Iterator it = myList.iterator(); while(it.hasNext()) { // 没有这个语法,通过迭代器取出的就是Object //Animal a = it.next(); Object obj = it.next(); //obj中没有move方法,无法调用,需要向下转型! if(obj instanceof Animal){ Animal a = (Animal)obj; a.move(); } } } } class Animal { // 父类自带方法 public void move(){ System.out.println("动物在移动!"); } } class Cat extends Animal { // 特有方法 public void catchMouse(){ System.out.println("猫抓老鼠!"); } } class Bird extends Animal { // 特有方法 public void fly(){ System.out.println("鸟儿在飞翔!"); } }
区别在于 如果使用了泛型机制的代码
可以不用强转化
但是调用子类的方法还是要进行强转换
// 使用JDK5之后的泛型机制 // 使用泛型List<Animal>之后,表示List集合中只允许存储Animal类型的数据。 // 用泛型来指定集合中存储的数据类型。 List<Animal> myList = new ArrayList<Animal>(); // 指定List集合中只能存储Animal,那么存储String就编译报错了。 // 这样用了泛型之后,集合中元素的数据类型更加统一了。 //myList.add("abc"); Cat c = new Cat(); Bird b = new Bird(); myList.add(c); myList.add(b); // 获取迭代器 // 这个表示迭代器迭代的是Animal类型。 Iterator<Animal> it = myList.iterator(); while(it.hasNext()){ // 使用泛型之后,每一次迭代返回的数据都是Animal类型。 //Animal a = it.next(); // 这里不需要进行强制类型转换了。直接调用。 //a.move(); // 调用子类型特有的方法还是需要向下转换的! Animal a = it.next(); if(a instanceof Cat) { Cat x = (Cat)a; x.catchMouse(); } if(a instanceof Bird) { Bird y = (Bird)a; y.fly(); }
ArrayList<这里的类型会自动推断>(),前提是JDK8之后才允许
List<Animal> myList = new ArrayList<>();
添加对应的类型
myList.add(new Animal());
myList.add(new Cat());
myList.add(new Bird());
// 遍历
Iterator<Animal> it = myList.iterator();
while(it.hasNext()){
Animal a = it.next();
a.move();
}
如果类型是string类型
则为
List<String> strList = new ArrayList<>();
// 类型不匹配。
//strList.add(new Cat());
strList.add("http://www.126.com");
strList.add("http://www.baidu.com");
strList.add("http://www.bjpowernode.com");
// 类型不匹配。
//strList.add(123);
如果使用了泛型机制则可以不用强转换,返回的类型是对应的
// 遍历 Iterator<String> it2 = strList.iterator(); while(it2.hasNext()){ // 如果没有使用泛型 /* Object obj = it2.next(); if(obj instanceof String){ String ss = (String)obj; ss.substring(7); } */ // 直接通过迭代器获取了String类型的数据 String s = it2.next(); // 直接调用String类的substring方法截取字符串。 String newString = s.substring(7); System.out.println(newString);
自定义泛型的时候,<> 尖括号中的是一个标识符,随便写。
java源代码中经常出现的是:<E>和<T>
。E是Element单词首字母。T是Type单词首字母。
根据自已定义的泛型,泛型可以随便写,但常用的就那两个字母
public class GenericTest03<标识符随便写> { public void doSome(标识符随便写 o){ System.out.println(o); } public static void main(String[] args) { // new对象的时候指定了泛型是:String类型 GenericTest03<String> gt = new GenericTest03<>(); // 类型不匹配 //gt.doSome(100); gt.doSome("abc"); // ============================================================= GenericTest03<Integer> gt2 = new GenericTest03<>(); gt2.doSome(100); // 类型不匹配 //gt2.doSome("abc"); MyIterator<String> mi = new MyIterator<>(); String s1 = mi.get(); MyIterator<Animal> mi2 = new MyIterator<>(); Animal a = mi2.get(); // 不用泛型就是Object类型。 /*GenericTest03 gt3 = new GenericTest03(); gt3.doSome(new Object());*/ } } class MyIterator<T> { public T get(){ return null; } }
foreach有一个缺点:没有下标。
在需要使用下标的循环中,不建议使用增强for循环
测试例子
public class ForEachTest01 { public static void main(String[] args) { // int类型数组 int[] arr = {432,4,65,46,54,76,54}; // 遍历数组(普通for循环) for(int i = 0; i < arr.length; i++) { System.out.println(arr[i]); } // 增强for(foreach) // 以下是语法 /*for(元素类型 变量名 : 数组或集合){ System.out.println(变量名); }*/ System.out.println("======================================"); // foreach有一个缺点:没有下标。在需要使用下标的循环中,不建议使用增强for循环。 for(int data : arr) { // data就是数组中的元素(数组中的每一个元素。) System.out.println(data); } } }
结合泛型的foreach结构类型例子
public class ForEachTest02 { public static void main(String[] args) { // 创建List集合 List<String> strList = new ArrayList<>(); // 添加元素 strList.add("hello"); strList.add("world!"); strList.add("kitty!"); // 遍历,使用迭代器方式 Iterator<String> it = strList.iterator(); while(it.hasNext()){ String s = it.next(); System.out.println(s); } // 使用下标方式(只针对于有下标的集合) for(int i = 0; i < strList.size(); i++){ System.out.println(strList.get(i)); } // 使用foreach for(String s : strList){ // 因为泛型使用的是String类型,所以是:String s System.out.println(s); } List<Integer> list = new ArrayList<>(); list.add(100); list.add(200); list.add(300); for(Integer i : list){ // i代表集合中的元素 System.out.println(i); } } }
无序不可重复
// 演示一下HashSet集合特点 Set<String> strs = new HashSet<>(); // 添加元素 strs.add("hello3"); strs.add("hello4"); strs.add("hello1"); strs.add("hello2"); strs.add("hello3"); strs.add("hello3"); strs.add("hello3"); strs.add("hello3"); // 遍历 /* hello1 hello4 hello2 hello3 */ for(String s : strs){ System.out.println(s);
无序不可重复的,但是存储的元素可以自动按照大小顺序排序
这里的无序指的是存进去的顺序和取出来的顺序不同。并且没有下标
// 创建集合对象 Set<String> strs = new TreeSet<>(); // 添加元素 strs.add("A"); strs.add("B"); strs.add("Z"); strs.add("Y"); strs.add("Z"); strs.add("K"); strs.add("M"); // 遍历 /* A B K M Y Z 从小到大自动排序! */ for(String s : strs){ System.out.println(s);
方法 | 表述 |
---|---|
V put(K key, V value) | 向Map集合中添加键值对 |
V get(Object key) | 通过key获取value |
void clear() | 清空Map集合 |
boolean containsKey(Object key) | 判断Map中是否包含某个key |
boolean containsValue(Object value) | 判断Map中是否包含某个value |
boolean isEmpty() | 判断Map集合中元素个数是否为0 |
V remove(Object key) | 通过key删除键值对 |
int size() | 获取Map集合中键值对的个数。 |
Collection values() | 获取Map集合中所有的value,返回一个Collection |
Set keySet() | 获取Map集合所有的key(所有的键是一个set集合) |
Set<Map.Entry<K,V>> entrySet() | 将Map集合转换成Set集合 |
主要的代码展示
public class MapTest01 { public static void main(String[] args) { // 创建Map集合对象 Map<Integer, String> map = new HashMap<>(); // 向Map集合中添加键值对 map.put(1, "zhangsan"); // 1在这里进行了自动装箱。 map.put(2, "lisi"); map.put(3, "wangwu"); map.put(4, "zhaoliu"); // 通过key获取value String value = map.get(2); System.out.println(value); // 获取键值对的数量 System.out.println("键值对的数量:" + map.size()); // 通过key删除key-value map.remove(2); System.out.println("键值对的数量:" + map.size()); // 判断是否包含某个key // contains方法底层调用的都是equals进行比对的,所以自定义的类型需要重写equals方法。 System.out.println(map.containsKey(new Integer(4))); // true // 判断是否包含某个value System.out.println(map.containsValue(new String("wangwu"))); // true // 获取所有的value Collection<String> values = map.values(); // foreach for(String s : values){ System.out.println(s); } // 清空map集合 map.clear(); System.out.println("键值对的数量:" + map.size()); // 判断是否为空 System.out.println(map.isEmpty()); // true } }
最后一个方法函数重点说明:
假设现在有一个Map集合,如下所示: map1集合对象 key value ---------------------------- 1 zhangsan 2 lisi 3 wangwu 4 zhaoliu Set set = map1.entrySet(); set集合对象 1=zhangsan 【注意:Map集合通过entrySet()方法转换成的这个Set集合,Set集合中元素的类型是 Map.Entry<K,V>】 2=lisi 【Map.Entry和String一样,都是一种类型的名字,只不过:Map.Entry是静态内部类,是Map中的静态内部类】 3=wangwu 4=zhaoliu ---> 这个东西是个什么?Map.Entry
所谓的静态内部类可以通过下面这个代码进行理解
public class MyClass { // 声明一个静态内部类 private static class InnerClass { // 静态方法 public static void m1(){ System.out.println("静态内部类的m1方法执行"); } // 实例方法 public void m2(){ System.out.println("静态内部类中的实例方法执行!"); } } public static void main(String[] args) { // 类名叫做:MyClass.InnerClass MyClass.InnerClass.m1(); // 创建静态内部类对象 MyClass.InnerClass mi = new MyClass.InnerClass(); mi.m2(); // 给一个Set集合 // 该Set集合中存储的对象是:MyClass.InnerClass类型 Set<MyClass.InnerClass> set = new HashSet<>(); // 这个Set集合中存储的是字符串对象。 Set<String> set2 = new HashSet<>(); Set<MyMap.MyEntry<Integer, String>> set3 = new HashSet<>(); } } class MyMap { public static class MyEntry<K,V> { } }
第一种方式:获取所有的key,通过遍历key,来遍历value
Map<Integer, String> map = new HashMap<>();
map.put(1, "zhangsan");
map.put(2, "lisi");
map.put(3, "wangwu");
map.put(4, "zhaoliu");
// 遍历Map集合
// 获取所有的key,所有的key是一个Set集合
Set<Integer> keys = map.keySet();
遍历key,通过key获取value
Iterator<Integer> it = keys.iterator();
while(it.hasNext()){
// 取出其中一个key
Integer key = it.next();
// 通过key获取value
String value = map.get(key);
System.out.println(key + "=" + value);
}
for(Integer key : keys){
System.out.println(key + "=" + map.get(key));
}
第二种方式:Set<Map.Entry<K,V>> entrySet()
以上这个方法是把Map集合直接全部转换成Set集合。
// Set集合中元素的类型是:Map.Entry Set<Map.Entry<Integer,String>> set = map.entrySet(); // 遍历Set集合,每一次取出一个Node // 迭代器 /*Iterator<Map.Entry<Integer,String>> it2 = set.iterator(); while(it2.hasNext()){ Map.Entry<Integer,String> node = it2.next(); Integer key = node.getKey(); String value = node.getValue(); System.out.println(key + "=" + value); }*/ // foreach // 这种方式效率比较高,因为获取key和value都是直接从node对象中获取的属性值。 // 这种方式比较适合于大数据量。 for(Map.Entry<Integer,String> node : set){ System.out.println(node.getKey() + "--->" + node.getValue()); }
public class HashMap{
// HashMap底层实际上就是一个数组。(一维数组)
Node<K,V>[] table;
// 静态的内部类HashMap.Node
static class Node<K,V> {
final int hash; // 哈希值(哈希值是key的hashCode()方法的执行结果。hash值通过哈希函数/算法,可以转换存储成数组的下标。)
final K key; // 存储到Map集合中的那个key
V value; // 存储到Map集合中的那个value
Node<K,V> next; // 下一个节点的内存地址。
}
}
哈希表/散列表:一维数组,这个数组中每一个元素是一个单向链表。(数组和链表的结合体。)
HashMap集合的key部分特点:无序,不可重复。
为什么无序? 因为不一定挂到哪个单向链表上。
不可重复是怎么保证的? equals方法来保证HashMap集合的key不可重复。如果key重复了,value会覆盖。
哈希表HashMap使用不当时无法发挥性能!
1.假设将所有的hashCode()方法返回值固定为某个值,那么会导致底层哈希表变成了纯单向链表。这种情况我们成为:散列分布不均匀。 什么是散列分布均匀?假设有100个元素,10个单向链表,那么每个单向链表上有10个节点,这是最好的, 是散列分布均匀的。
2.假设将所有的hashCode()方法返回值都设定为不一样的值,这样的话导致底层哈希表就成为一维数组了,没有链表的概念了。也是散列分布不均匀。
散列分布均匀需要你重写hashCode()方法时有一定的技巧。
HashMap集合的默认初始化容量是16,默认加载因子是0.75
这个默认加载因子是当HashMap集合底层数组的容量达到75%的时候,数组开始扩容。HashMap集合初始化容量必须是2的倍数,这也是官方推荐的,这是因为达到散列均匀,为了提高HashMap集合的存取效率,所必须的。
最主要掌握的是:
map.put(k,v)
v = map.get(k)
以上这两个方法的实现原理
测试HashMap集合key部分的元素特点
// Integer是key,它的hashCode和equals都重写了。
Map<Integer,String> map = new HashMap<>();
map.put(1111, "zhangsan");
map.put(6666, "lisi");
map.put(7777, "wangwu");
map.put(2222, "zhaoliu");
map.put(2222, "king"); //key重复的时候value会自动覆盖。
System.out.println(map.size()); // 4
// 遍历Map集合
Set<Map.Entry<Integer,String>> set = map.entrySet();
for(Map.Entry<Integer,String> entry : set){
// 验证结果:HashMap集合key部分元素:无序不可重复。
System.out.println(entry.getKey() + "=" + entry.getValue());
此处重点讲解一下
向Map集合中存,以及从Map集合中取,都是先调用key的hashCode方法,然后再调用equals方法!
equals方法有可能调用,也有可能不调用。
put(k,v)和get(k)举例,什么时候equals不会调用? k.hashCode()方法返回哈希值,哈希值经过哈希算法转换成数组下标。数组下标位置上如果是null,equals不需要执行。
注意:如果一个类的equals方法重写了,那么hashCode()方法必须重写。
并且equals方法返回如果是true,hashCode()方法返回的值必须一样。
equals方法返回true表示两个对象相同,在同一个单向链表上比较。
那么对于同一个单向链表上的节点来说,他们的哈希值都是相同的。
所以hashCode()方法的返回值也应该相同。
对于哈希表数据结构来说:
如果o1和o2的hash值相同,一定是放到同一个单向链表上。
当然如果o1和o2的hash值不同,但由于哈希算法执行结束之后转换的数组下标可能相同,此时会发生“哈希碰撞”。
重写equals与hashcode两者进行比较的测试代码
Student s1 = new Student("zhangsan"); Student s2 = new Student("zhangsan"); // 重写equals方法之前是false //System.out.println(s1.equals(s2)); // false // 重写equals方法之后是true System.out.println(s1.equals(s2)); //true (s1和s2表示相等) System.out.println("s1的hashCode=" + s1.hashCode()); //284720968 (重写hashCode之后-1432604525) System.out.println("s2的hashCode=" + s2.hashCode()); //122883338 (重写hashCode之后-1432604525) // s1.equals(s2)结果已经是true了,表示s1和s2是一样的,相同的,那么往HashSet集合中放的话, // 按说只能放进去1个。(HashSet集合特点:无序不可重复) Set<Student> students = new HashSet<>(); students.add(s1); students.add(s2); System.out.println(students.size()); // 这个结果按说应该是1. 但是结果是2.显然不符合HashSet集合存储特点。怎么办?
重写的类主要代码
public class Student { private String name; public Student() { } public Student(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } // hashCode // equals(如果学生名字一样,表示同一个学生。) /*public boolean equals(Object obj){ if(obj == null || !(obj instanceof Student)) return false; if(obj == this) return true; Student s = (Student)obj; return this.name.equals(s.name); }*/ @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return Objects.equals(name, student.name); } @Override public int hashCode() { return Objects.hash(name); } }
Hashtable的key和value都是不能为null的。
HashMap集合的key和value都是可以为null的。
Hashtable方法都带有synchronized:线程安全的。
线程安全有其它的方案,这个Hashtable对线程的处理
导致效率较低,使用较少了。
Hashtable和HashMap一样,底层都是哈希表数据结构。
Hashtable的初始化容量是11,默认加载因子是:0.75f
Hashtable的扩容是:原容量 * 2 + 1
这个会出错
Map map = new Hashtable();
//map.put(null, "123");
map.put(100, null);
这个不会出错
Map map = new HashMap();
// HashMap集合允许key为null
map.put(null, null);
System.out.println(map.size()); // 1
// key重复的话value是覆盖!
map.put(null, 100);
System.out.println(map.size()); //1
// 通过key获取value
System.out.println(map.get(null)); // 100
Properties是一个Map集合,继承Hashtable,Properties的key和value都是String类型。
Properties被称为属性类对象。
Properties是线程安全的。
// 创建一个Properties对象 Properties pro = new Properties(); // 需要掌握Properties的两个方法,一个存,一个取。 pro.setProperty("url", "jdbc:mysql://localhost:3306/bjpowernode"); pro.setProperty("driver","com.mysql.jdbc.Driver"); pro.setProperty("username", "root"); pro.setProperty("password", "123"); // 通过key获取value String url = pro.getProperty("url"); String driver = pro.getProperty("driver"); String username = pro.getProperty("username"); String password = pro.getProperty("password"); System.out.println(url); System.out.println(driver); System.out.println(username); System.out.println(password);
// 创建一个TreeSet集合 TreeSet<String> ts = new TreeSet<>(); // 添加String ts.add("zhangsan"); ts.add("lisi"); ts.add("wangwu"); ts.add("zhangsi"); ts.add("wangliu"); // 遍历 for(String s : ts){ // 按照字典顺序,升序! System.out.println(s); } TreeSet<Integer> ts2 = new TreeSet<>(); ts2.add(100); ts2.add(200); ts2.add(900); ts2.add(800); ts2.add(600); ts2.add(10); for(Integer elt : ts2){ // 升序! System.out.println(elt);
对自定义的类型来说,TreeSet可以排序吗?
以下程序中对于Person类型来说,无法排序。因为没有指定Person对象之间的比较规则
以下程序运行的时候出现了这个异常:
java.lang.ClassCastException:
class com.bjpowernode.javase.collection.Person
cannot be cast to class java.lang.Comparable
出现这个异常的原因是:
Person类没有实现java.lang.Comparable接口。
程序如下
public class TreeSetTest03 { public static void main(String[] args) { person p1 = new person(32); //System.out.println(p1); person p2 = new person(20); person p3 = new person(30); person p4 = new person(25); // 创建TreeSet集合 TreeSet<person> persons = new TreeSet<>(); // 添加元素 persons.add(p1); persons.add(p2); persons.add(p3); persons.add(p4); // 遍历 for (person p : persons){ System.out.println(p); } } } class Person { int age; public Person(int age){ this.age = age; } // 重写toString()方法 public String toString(){ return "Person[age="+age+"]"; } }
第一种改写
放在TreeSet集合中的元素需要实现java.lang.Comparable接口
需要在这个方法中编写比较的逻辑,或者说比较的规则,按照什么进行比较!
class Customer implements Comparable<Customer>{ int age; public Customer(int age){ this.age = age; } // k.compareTo(t.key) // 拿着参数k和集合中的每一个k进行比较,返回值可能是>0 <0 =0 // 比较规则最终还是由程序员指定的:例如按照年龄升序。或者按照年龄降序。 @Override public int compareTo(Customer c) { // c1.compareTo(c2); //return this.age - c.age; // =0 >0 <0 return c.age - this.age; } public String toString(){ return "Customer[age="+age+"]"; } } //具体改写的接口也可以这样定义 // this是c1 // c是c2 // c1和c2比较的时候,就是this和c比较。 /*int age1 = this.age; int age2 = c.age; if(age1 == age2){ return 0; } else if(age1 > age2) { return 1; } else { return -1; }*/
实现类如下
Customer c1 = new Customer(32); Customer c2 = new Customer(20); Customer c3 = new Customer(30); Customer c4 = new Customer(25); // 创建TreeSet集合 TreeSet<Customer> customers = new TreeSet<>(); // 添加元素 customers.add(c1); customers.add(c2); customers.add(c3); customers.add(c4); // 遍历 for (Customer c : customers){ System.out.println(c);
另一个完整的案例可以参照如下程序
先按照年龄升序,如果年龄一样的再按照姓名升序
public class TreeSetTest05 { public static void main(String[] args) { TreeSet<Vip> vips = new TreeSet<>(); vips.add(new Vip("zhangsi", 20)); vips.add(new Vip("zhangsan", 20)); vips.add(new Vip("king", 18)); vips.add(new Vip("soft", 17)); for(Vip vip : vips){ System.out.println(vip); } } } class Vip implements Comparable<Vip>{ String name; int age; public Vip(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Vip{" + "name='" + name + '\'' + ", age=" + age + '}'; } /* compareTo方法的返回值很重要: 返回0表示相同,value会覆盖。 返回>0,会继续在右子树上找。【10 - 9 = 1 ,1 > 0的说明左边这个数字比较大。所以在右子树上找。】 返回<0,会继续在左子树上找。 */ @Override public int compareTo(Vip v) { // 写排序规则,按照什么进行比较。 if(this.age == v.age){ // 年龄相同时按照名字排序。 // 姓名是String类型,可以直接比。调用compareTo来完成比较。 return this.name.compareTo(v.name); } else { // 年龄不一样 return this.age - v.age; } } }
第二种改写的方式
使用比较器的方式
比较器要传入参数进去
TreeSet<WuGui> wuGuis = new TreeSet<>();//这样不行,没有通过构造方法传递一个比较器进去
完整代码案例如下
public class TreeSetTest06 { public static void main(String[] args) { // 创建TreeSet集合的时候,需要使用这个比较器。 // TreeSet<WuGui> wuGuis = new TreeSet<>();//这样不行,没有通过构造方法传递一个比较器进去。 // 给构造方法传递一个比较器。 //TreeSet<WuGui> wuGuis = new TreeSet<>(new WuGuiComparator()); // 大家可以使用匿名内部类的方式(这个类没有名字。直接new接口。) TreeSet<WuGui> wuGuis = new TreeSet<>(new Comparator<WuGui>() { @Override public int compare(WuGui o1, WuGui o2) { return o1.age - o2.age; } }); wuGuis.add(new WuGui(1000)); wuGuis.add(new WuGui(800)); wuGuis.add(new WuGui(810)); for(WuGui wuGui : wuGuis){ System.out.println(wuGui); } } } // 乌龟 class WuGui{ int age; public WuGui(int age){ this.age = age; } @Override public String toString() { return "小乌龟[" + "age=" + age + ']'; } } // 单独在这里编写一个比较器 // 比较器实现java.util.Comparator接口。(Comparable是java.lang包下的。Comparator是java.util包下的。) /* class WuGuiComparator implements Comparator<WuGui> { @Override public int compare(WuGui o1, WuGui o2) { // 指定比较规则 // 按照年龄排序 return o1.age - o2.age; } } */
最终的结论:
放到TreeSet或者TreeMap集合key部分的元素要想做到排序,包括两种方式:
第一种:放在集合中的元素实现java.lang.Comparable接口。
第二种:在构造TreeSet或者TreeMap集合的时候给它传一个比较器对象。
Comparable和Comparator怎么选择呢?
当比较规则不会发生改变的时候,或者说当比较规则只有1个的时候,建议实现Comparable接口。
如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用Comparator接口。
Comparator接口的设计符合OCP原则。
java.util.Collection 集合接口
java.util.Collections 集合工具类,方便集合的操作。
线程安全的集合转换为非线程安全
// ArrayList集合不是线程安全的。
List<String> list = new ArrayList<>();
// 变成线程安全的
Collections.synchronizedList(list);
运用排序的函数
// 排序
list.add("abf");
list.add("abx");
list.add("abc");
list.add("abe");
Collections.sort(list);
for(String s : list){
System.out.println(s);
}
自定义的要重写Comparable接口
List<WuGui2> wuGuis = new ArrayList<>(); wuGuis.add(new WuGui2(1000)); wuGuis.add(new WuGui2(8000)); wuGuis.add(new WuGui2(500)); // 注意:对List集合中元素排序,需要保证List集合中的元素实现了:Comparable接口。 Collections.sort(wuGuis); for(WuGui2 wg : wuGuis){ System.out.println(wg); } class WuGui2 implements Comparable<WuGui2>{ int age; public WuGui2(int age){ this.age = age; } @Override public int compareTo(WuGui2 o) { return this.age - o.age; } @Override public String toString() { return "WuGui2{" + "age=" + age + '}'; } }
set集合如何进行排序
// 对Set集合怎么排序呢?
Set<String> set = new HashSet<>();
set.add("king");
set.add("kingsoft");
set.add("king2");
set.add("king1");
// 将Set集合转换成List集合
List<String> myList = new ArrayList<>(set);
Collections.sort(myList);
for(String s : myList) {
System.out.println(s);
}
// 这种方式也可以排序。
//Collections.sort(list集合, 比较器对象);
一共有以上的集合元素
注意考察代码功能是
关于概念的总结,map这一块比较多,因为面试实在是太常考了
HashSet 如何检查重复
当你把对象加⼊ HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加⼊的位置,同时也会与其他加⼊的对象的 hashcode 值作⽐较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调⽤ equals() ⽅法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加⼊操作成功。
关于map的比较:
HashMap 和 Hashtable 的区别
HashMap 的底层实现
JDK1.8 之前 HashMap 底层是 数组和链表 结合在⼀起使⽤也就是 链表散列
JDK1.8 之后在解决哈希冲突时有了较⼤的变化,当链表⻓度⼤于阈值(默认为8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间
TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都⽤到了红⿊树。红⿊树就是为了解决⼆叉
查找树的缺陷,因为⼆叉查找树在某些情况下会退化成⼀个线性结构
HashMap 的⻓度为什么是 2 的幂次⽅
为了能让 HashMap 存取⾼效,尽量较少碰撞,也就是要尽量把数据分配均匀
⽤之前还要先做对数组的⻓度取模运算,得到的余数才能⽤来要存放的位置也就是对应的数组下标。这个数组下标的计算⽅法是“ (n - 1) & hash ”。
hash%length==hash&(length-1)的前提是 length 是 2的 幂次⽅
ConcurrentHashMap 和 Hashtable 的区别
底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采⽤ 分段的数组+链表 实现,JDK1.8 采⽤的数据结构跟 HashMap1.8 的结构⼀样,数组+链表/红⿊⼆叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采⽤ 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的
实现线程安全的⽅式(重要): ① 在 JDK1.7 的时候,ConcurrentHashMap(分段锁) 对整个桶数组进⾏了分割分段(Segment),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment 的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,并发控制使⽤synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同⼀把锁) :使⽤ synchronized 来保证线程安全,效率⾮常低下。当⼀个线程访问同步⽅法时,其他线程也访问同步⽅法,可能会进⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不能使⽤ put 添加元素,也不能使⽤get,竞争会越来越激烈效率越低
ArrayList和linkedlist
// 创建集合对象 //ArrayList<String> list = new ArrayList<>(); LinkedList<String> list = new LinkedList<>(); // 添加元素 list.add("zhangsan"); list.add("lisi"); list.add("wangwu"); // 从集合中取出某个元素 // List集合有下标 String firstElt = list.get(0); System.out.println(firstElt); // 遍历(下标方式) for(int i = 0; i < list.size(); i++){ String elt = list.get(i); System.out.println(elt); } // 遍历(迭代器方式,这个是通用的,所有Collection都能用) Iterator<String> it = list.iterator(); while(it.hasNext()){ System.out.println(it.next()); } // while循环修改为for循环 /*for(Iterator<String> it2 = list.iterator(); it2.hasNext(); ){ System.out.println("====>" + it2.next()); }*/ // 遍历(foreach方式) for(String s : list){ System.out.println(s);
HashSet
public class HashSetTest { public static void main(String[] args) { // 创建集合对象 HashSet<String> set = new HashSet<>(); // 添加元素 set.add("abc"); set.add("def"); set.add("king"); // set集合中的元素不能通过下标取了。没有下标 // 遍历集合(迭代器) Iterator<String> it = set.iterator(); while(it.hasNext()){ System.out.println(it.next()); } // 遍历集合(foreach) for(String s : set){ System.out.println(s); } set.add("king"); set.add("king"); set.add("king"); System.out.println(set.size()); //3 (后面3个king都没有加进去。) set.add("1"); set.add("10"); set.add("2"); for(String s : set){ System.out.println("--->" + s); } // 创建Set集合,存储Student数据 Set<Student> students = new HashSet<>(); Student s1 = new Student(111, "zhangsan"); Student s2 = new Student(222, "lisi"); Student s3 = new Student(111, "zhangsan"); students.add(s1); students.add(s2); students.add(s3); System.out.println(students.size()); // 2 // 遍历 for(Student stu : students){ System.out.println(stu); } } } class Student { int no; String name; public Student() { } public Student(int no, String name) { this.no = no; this.name = name; } @Override public String toString() { return "Student{" + "no=" + no + ", name='" + name + '\'' + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return no == student.no && Objects.equals(name, student.name); } @Override public int hashCode() { return Objects.hash(no, name); } }
hashmap
// 创建Map集合 Map<Integer, String> map = new HashMap<>(); // 添加元素 map.put(1, "zhangsan"); map.put(9, "lisi"); map.put(10, "wangwu"); map.put(2, "king"); map.put(2, "simth"); // key重复value会覆盖。 // 获取元素个数 System.out.println(map.size()); // 取key是2的元素 System.out.println(map.get(2)); // smith // 遍历Map集合很重要,几种方式都要会。 // 第一种方式:先获取所有的key,遍历key的时候,通过key获取value Set<Integer> keys = map.keySet(); for(Integer key : keys){ System.out.println(key + "=" + map.get(key)); } // 第二种方式:是将Map集合转换成Set集合,Set集合中每一个元素是Node // 这个Node节点中有key和value Set<Map.Entry<Integer,String>> nodes = map.entrySet(); for(Map.Entry<Integer,String> node : nodes){ System.out.println(node.getKey() + "=" + node.getValue()); }
properties
// 创建对象
Properties pro = new Properties();
// 存
pro.setProperty("username", "test");
pro.setProperty("password", "test123");
// 取
String username = pro.getProperty("username");
String password = pro.getProperty("password");
System.out.println(username);
System.out.println(password);
TreeSet
这个比较特殊
public class TreeSetTest { public static void main(String[] args) { // 集合的创建(可以测试以下TreeSet集合中存储String、Integer的。这些类都是SUN写好的。) //TreeSet<Integer> ts = new TreeSet<>(); // 编写比较器可以改变规则。 TreeSet<Integer> ts = new TreeSet<>(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; // 自动拆箱 } }); // 添加元素 ts.add(1); ts.add(100); ts.add(10); ts.add(10); ts.add(10); ts.add(10); ts.add(0); // 遍历(迭代器方式) Iterator<Integer> it = ts.iterator(); while(it.hasNext()) { Integer i = it.next(); System.out.println(i); } // 遍历(foreach) for(Integer x : ts){ System.out.println(x); } // TreeSet集合中存储自定义类型 TreeSet<A> atree = new TreeSet<>(); atree.add(new A(100)); atree.add(new A(200)); atree.add(new A(500)); atree.add(new A(300)); atree.add(new A(400)); atree.add(new A(1000)); // 遍历 for(A a : atree){ System.out.println(a); } //TreeSet<B> btree = new TreeSet<>(new BComparator()); // 匿名内部类方式。 TreeSet<B> btree = new TreeSet<>(new Comparator<B>() { @Override public int compare(B o1, B o2) { return o1.i - o2.i; } }); btree.add(new B(500)); btree.add(new B(100)); btree.add(new B(200)); btree.add(new B(600)); btree.add(new B(300)); btree.add(new B(50)); for(B b : btree){ System.out.println(b); } } } // 第一种方式:实现Comparable接口 class A implements Comparable<A>{ int i; public A(int i){ this.i = i; } @Override public String toString() { return "A{" + "i=" + i + '}'; } @Override public int compareTo(A o) { //return this.i - o.i; return o.i - this.i; } } class B { int i; public B(int i){ this.i = i; } @Override public String toString() { return "B{" + "i=" + i + '}'; } } // 比较器 class BComparator implements Comparator<B> { @Override public int compare(B o1, B o2) { return o1.i - o2.i; } }
I : Input
O : Output
通过IO可以完成硬盘文件的读和写
流的分类:
一种方式是按照流的方向进行分类:以内存作为参照物,往内存中去,叫做输入(Input)。或者叫做读(Read)。从内存中出来,叫做输出(Output)。或者叫做写(Write)。
另一种方式是按照读取数据方式不同进行分类:按照字节的方式读取数据,按照字符的方式读取数据的
以上都是都是抽象类。(abstract class),所有的流都实现java.io.Closeable接口,都是可关闭的,都有close()方法。所有的输出流都实现java.io.Flushable接口,都是可刷新的,都有flush()方法,这个刷新表示将通道/管道当中剩余未输出的数据强行输出完(清空管道!)刷新的作用就是清空管道。注意:如果没有flush()可能会导致丢失数据
文件专属: java.io.FileInputStream(掌握) java.io.FileOutputStream(掌握) java.io.FileReader java.io.FileWriter 转换流:(将字节流转换成字符流) java.io.InputStreamReader java.io.OutputStreamWriter 缓冲流专属: java.io.BufferedReader java.io.BufferedWriter java.io.BufferedInputStream java.io.BufferedOutputStream 数据流专属: java.io.DataInputStream java.io.DataOutputStream 标准输出流: java.io.PrintWriter java.io.PrintStream(掌握) 对象专属流: java.io.ObjectInputStream(掌握) java.io.ObjectOutputStream(掌握)
只要“类名”以Stream结尾的都是字节流。以“Reader/Writer”结尾的都是字符流
采用这种int readData = fis.read();
一次读取一个字节byte,这样内存和硬盘交互太频繁,基本上时间/资源都耗费,读取文件路径的时候使用绝对路径,(IDEA会自动把\编程\,因为java中\表示转义,也可以使用/)
案例如下:
public class FileInputStreamTest02 { public static void main(String[] args) { FileInputStream fis = null; try { fis = new FileInputStream("D:\\temp"); /*while(true) { int readData = fis.read(); if(readData == -1) { break; } System.out.println(readData); }*/ // 改造while循环 int readData = 0; while((readData = fis.read()) != -1){ System.out.println(readData); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
为了改进一次只读取一个数据
可以使用一次最多读取 b.length 个字节。减少硬盘和内存的交互,提高程序的执行效率。往byte[]数组当中读。
相对路径在工程Project的根就是IDEA的默认当前路径
// 开始读,采用byte数组,一次读取多个字节。最多读取“数组.length”个字节。 byte[] bytes = new byte[4]; // 准备一个4个长度的byte数组,一次最多读取4个字节。 // 这个方法的返回值是:读取到的字节数量。(不是字节本身) int readCount = fis.read(bytes); System.out.println(readCount); // 第一次读到了4个字节。 // 将字节数组全部转换成字符串 //System.out.println(new String(bytes)); // abcd // 不应该全部都转换,应该是读取了多少个字节,转换多少个。 System.out.println(new String(bytes,0, readCount)); readCount = fis.read(bytes); // 第二次只能读取到2个字节。 System.out.println(readCount); // 2 // 将字节数组全部转换成字符串 //System.out.println(new String(bytes)); // efcd // 不应该全部都转换,应该是读取了多少个字节,转换多少个。 System.out.println(new String(bytes,0, readCount)); readCount = fis.read(bytes); // 1个字节都没有读取到返回-1 System.out.println(readCount); // -1
为了改进上面这种算法采用有多少读取多少
FileInputStream fis = null; try { fis = new FileInputStream("chapter23/src/tempfile3"); // 准备一个byte数组 byte[] bytes = new byte[4]; /*while(true){ int readCount = fis.read(bytes); if(readCount == -1){ break; } // 把byte数组转换成字符串,读到多少个转换多少个。 System.out.print(new String(bytes, 0, readCount)); }*/ int readCount = 0; while((readCount = fis.read(bytes)) != -1) { System.out.print(new String(bytes, 0, readCount)); }
模板
byte[] bytes = new byte[4];
int readCount = 0;
while((readCount = fis.read(bytes)) != -1) {
System.out.print(new String(bytes, 0, readCount));
}
其他常用方法
方法 | 功能 |
---|---|
int available() | 返回流当中剩余的没有读到的字节数量 |
long skip(long n) | 跳过几个字节不读 |
这些方法的具体使用如下
主要是一次性获取
不太适合太大的文件,因为byte[]数组不能太大。
byte[] bytes = new byte[fis.available()];
int readCount = fis.read(bytes); // 6
System.out.println(new String(bytes)); // abcdef
// skip跳过几个字节不读取,这个方法也可能以后会用!
fis.skip(3);
System.out.println(fis.read()); //100
myfile文件不存在的时候会自动新建fos = new FileOutputStream("myfile");
如果没有true追加的形式,则会直接覆盖掉原来的东西
// 以追加的方式在文件末尾写入。不会清空原文件内容。
fos = new FileOutputStream("chapter23/src/tempfile3", true);
// 开始写。
byte[] bytes = {97, 98, 99, 100};
// 将byte数组全部写出!
fos.write(bytes); // abcd
// 将byte数组的一部分写出!
fos.write(bytes, 0, 2); // 再写出ab
将字符串转换为byte数组,进行写入
// 字符串
String s = "我是一个中国人,我骄傲!!!";
// 将字符串转换成byte数组。
byte[] bs = s.getBytes();
// 写
fos.write(bs);
每一次的写入都要记得刷新fos.flush();
而且在finally之后要关闭
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
文件字符输入流,只能读取普通文本。
读取文本内容时,比较方便,快捷。
主要的核心代码如下
读取指定的单一字符遍历输出:
//准备一个char数组
char[] chars = new char[4];
// 往char数组中读
reader.read(chars); // 按照字符的方式读取:第一次e,第二次f,第三次 风....
for(char c : chars) {
System.out.println(c);
}
或者是根据文件获取的字符数一直while,比较方便,当为-1的时候结束输出
// 开始读
char[] chars = new char[4]; // 一次读取4个字符
int readCount = 0;
while((readCount = reader.read(chars)) != -1) {
System.out.print(new String(chars,0,readCount));
}
FileWriter:
文件字符输出流。写。只能输出普通文本。
核心代码如下:
追加与不追加,覆盖的区别
// 创建文件字符输出流对象
//out = new FileWriter("file");
out = new FileWriter("file", true);
书写的核心文件如下
// 开始写。
char[] chars = {'我','是','中','国','人'};
out.write(chars);
out.write(chars, 2, 3);
也可以单独输出一个字符串
out.write("我是一名java软件工程师!");
// 写出一个换行符。
out.write("\n");
out.write("hello world!");
每一次的写入都要记得刷新fos.flush();
而且在finally之后要关闭
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
字节流的文件复制
使用FileInputStream + FileOutputStream完成文件的拷贝。
拷贝的过程应该是一边读,一边写。
使用以上的字节流拷贝文件的时候,文件类型随意,万能的。什么样的文件都能拷贝。
完整代码如下,模板
public class Copy01 { public static void main(String[] args) { FileInputStream fis = null; FileOutputStream fos = null; try { // 创建一个输入流对象 fis = new FileInputStream(""); // 创建一个输出流对象 fos = new FileOutputStream(""); // 最核心的:一边读,一边写 byte[] bytes = new byte[1024 * 1024]; // 1MB(一次最多拷贝1MB。) int readCount = 0; while((readCount = fis.read(bytes)) != -1) { fos.write(bytes, 0, readCount); } // 刷新,输出流最后要刷新 fos.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { // 分开try,不要一起try。 // 一起try的时候,其中一个出现异常,可能会影响到另一个流的关闭。 if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
注意区分两者的不同
字符流的文件复制
使用FileReader FileWriter进行拷贝的话,只能拷贝“普通文本”文件。
完整代码如下
public class Copy02 { public static void main(String[] args) { FileReader in = null; FileWriter out = null; try { // 读 in = new FileReader("chapter23/src/com/bjpowernode/java/io/Copy02.java"); // 写 out = new FileWriter("Copy02.java"); // 一边读一边写: char[] chars = new char[1024 * 512]; // 1MB int readCount = 0; while((readCount = in.read(chars)) != -1){ out.write(chars, 0, readCount); } // 刷新 out.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
带有缓冲区的字符输入流
通过这个 BufferedReader br = new BufferedReader(reader);
本身这个函数内部是不可以使用reader的,因为是抽象类,必须要找抽象类的实现类,所以在这之前要FileReader reader = new FileReader("Copy02.java");
当一个流的构造方法中需要一个流的时候,这个被传进来的流叫做:节点流。外部负责包装的这个流,叫做:包装流,还有一个名字叫做:处理流。
像当前这个程序来说:FileReader就是一个节点流。BufferedReader就是包装流/处理流。
关闭的时候只需要关闭外层流就可,因为内部流in会自动关闭
具体的测试文件如下
FileReader reader = new FileReader("Copy02.java"); BufferedReader br = new BufferedReader(reader); // 读一行 String firstLine = br.readLine(); System.out.println(firstLine); // br.readLine()方法读取一个文本行,但不带换行符。 String s = null; while((s = br.readLine()) != null){ System.out.println(s); } // 关闭流 // 对于包装流来说,只需要关闭最外层流就行,里面的节点流会自动关闭。(可以看源代码。) br.close();
如果传输的是字节流,需要将字节流转换为字符流使用InputStreamReader
具体测试代码如下
FileInputStream in = new FileInputStream("Copy02.java"); // 通过转换流转换(InputStreamReader将字节流转换成字符流。) // in是节点流。reader是包装流。 InputStreamReader reader = new InputStreamReader(in); // 这个构造方法只能传一个字符流。不能传字节流。 BufferedReader br = new BufferedReader(reader);*/ // 合并 BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("Copy02.java"))); String line = null; while((line = br.readLine()) != null){ System.out.println(line); } // 关闭最外层 br.close();
只要是写的,都要在后面记得刷新
带有缓冲区的字符输出流
//BufferedWriter out = new BufferedWriter(new FileWriter("copy"));
// 开始写。
out.write("hello world!");
out.write("\n");
out.write("hello kitty!");
// 刷新
out.flush();
// 关闭最外层
out.close();
如果是带有字节流的,可以将其转换为字符流
true
为追加文件末尾而不是覆盖
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("copy", true)));
字节数据输出流
java.io.DataOutputStream:数据专属的流。
这个流可以将数据连同数据的类型一并写入文件。
注意:这个文件不是普通文本文档。(这个文件使用记事本打不开。)
// 创建数据专属的字节输出流
本身是接口类,不可以直接new,必须接一个接口实现类
DataOutputStream dos = new DataOutputStream(new FileOutputStream("data"));
比如测试代码如下
// 写数据 byte b = 100; short s = 200; int i = 300; long l = 400L; float f = 3.0F; double d = 3.14; boolean sex = false; char c = 'a'; // 写 dos.writeByte(b); // 把数据以及数据的类型一并写入到文件当中。 dos.writeShort(s); dos.writeInt(i); dos.writeLong(l); dos.writeFloat(f); dos.writeDouble(d); dos.writeBoolean(sex); dos.writeChar(c); // 刷新 dos.flush(); // 关闭最外层 dos.close();
字节数据输入流
DataInputStream:数据字节输入流。
DataOutputStream写的文件,只能使用DataInputStream去读。并且读的时候你需要提前知道写入的顺序。
读的顺序需要和写的顺序一致。才可以正常取出数据。
测试文件同理如下
DataInputStream dis = new DataInputStream(new FileInputStream("data")); // 开始读 byte b = dis.readByte(); short s = dis.readShort(); int i = dis.readInt(); long l = dis.readLong(); float f = dis.readFloat(); double d = dis.readDouble(); boolean sex = dis.readBoolean(); char c = dis.readChar(); System.out.println(b); System.out.println(s); System.out.println(i + 1000); System.out.println(l); System.out.println(f); System.out.println(d); System.out.println(sex); System.out.println(c); dis.close();
创建一个File对象
File f1 = new File("D:\\file");
判断是否存在!
System.out.println(f1.exists());
// 如果D:\file不存在,则以文件的形式创建出来 if(!f1.exists()) { // 以文件形式新建 f1.createNewFile(); } // 如果D:\file不存在,则以目录的形式创建出来 if(!f1.exists()) { // 以目录的形式新建。 f1.mkdir(); } // 可以创建多重目录吗? File f2 = new File("D:/a/b/c/d/e/f"); if(!f2.exists()) { // 多重目录的形式新建。 f2.mkdirs(); } File f3 = new File("D:\\course\\01-开课\\学习方法.txt"); // 获取文件的父路径 String parentPath = f3.getParent(); System.out.println(parentPath); //D:\course\01-开课 File parentFile = f3.getParentFile(); System.out.println("获取绝对路径:" + parentFile.getAbsolutePath()); File f4 = new File("copy"); System.out.println("绝对路径:" + f4.getAbsolutePath()); // C:\Users\Administrator\IdeaProjects\javase\copy
常用方法
示例代码
File f1 = new File("D:\\course\\01-开课\\开学典礼.ppt"); // 获取文件名 System.out.println("文件名:" + f1.getName()); // 判断是否是一个目录 System.out.println(f1.isDirectory()); // false // 判断是否是一个文件 System.out.println(f1.isFile()); // true // 获取文件最后一次修改时间 long haoMiao = f1.lastModified(); // 这个毫秒是从1970年到现在的总毫秒数。 // 将总毫秒数转换成日期????? Date time = new Date(haoMiao); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS"); String strTime = sdf.format(time); System.out.println(strTime); // 获取文件大小 System.out.println(f1.length()); //216064字节。
获取所有子文件
使用File中的listFiles
方法
使用 File[]
数组来接收
// File[] listFiles()
// 获取当前目录下所有的子文件。
File f = new File("D:\\course\\01-开课");
File[] files = f.listFiles();
// foreach
for(File file : files){
//System.out.println(file.getAbsolutePath());
System.out.println(file.getName());
/* 拷贝目录 */ public class CopyAll { public static void main(String[] args) { // 拷贝源 File srcFile = new File("D:\\course\\02-JavaSE\\document"); // 拷贝目标 File destFile = new File("C:\\a\\b\\c"); // 调用方法拷贝 copyDir(srcFile, destFile); } /** * 拷贝目录 * @param srcFile 拷贝源 * @param destFile 拷贝目标 */ private static void copyDir(File srcFile, File destFile) { if(srcFile.isFile()) { // srcFile如果是一个文件的话,递归结束。 // 是文件的时候需要拷贝。 // ....一边读一边写。 FileInputStream in = null; FileOutputStream out = null; try { // 读这个文件 // D:\course\02-JavaSE\document\JavaSE进阶讲义\JavaSE进阶-01-面向对象.pdf in = new FileInputStream(srcFile); // 写到这个文件中 // C:\course\02-JavaSE\document\JavaSE进阶讲义\JavaSE进阶-01-面向对象.pdf String path = (destFile.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath() : destFile.getAbsolutePath() + "\\") + srcFile.getAbsolutePath().substring(3); out = new FileOutputStream(path); // 一边读一边写 byte[] bytes = new byte[1024 * 1024]; // 一次复制1MB int readCount = 0; while((readCount = in.read(bytes)) != -1){ out.write(bytes, 0, readCount); } out.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } } return; } // 获取源下面的子目录 File[] files = srcFile.listFiles(); for(File file : files){ // 获取所有文件的(包括目录和文件)绝对路径 //System.out.println(file.getAbsolutePath()); if(file.isDirectory()){ // 新建对应的目录 //System.out.println(file.getAbsolutePath()); //D:\course\02-JavaSE\document\JavaSE进阶讲义 源目录 //C:\course\02-JavaSE\document\JavaSE进阶讲义 目标目录 String srcDir = file.getAbsolutePath(); String destDir = (destFile.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath() : destFile.getAbsolutePath() + "\\") + srcDir.substring(3); File newFile = new File(destDir); if(!newFile.exists()){ newFile.mkdirs(); } } // 递归调用 copyDir(file, destFile); } } }
经常改变的数据,可以单独写到一个文件中,使用程序动态读取
将来只需要修改这个文件的内容,java代码不需要改动,不需要重新编译,服务器也不需要重启。就可以拿到动态的信息
配置文件中的内容格式是:
key1=value
key2=value
属性配置文件建议以.properties结尾,但这不是必须的。
这种以.properties结尾的文件在java中被称为:属性配置文件。
其中Properties是专门存放属性配置文件内容的一个类
主要的代码思路是
新建一个输入流对象
创建一个集合,运用结合中的load方法将其输入流对象加载到集合中
之后运用集合的key与value进行获取
public class IoPropertiesTest01 { public static void main(String[] args) throws Exception{ /* Properties是一个Map集合,key和value都是String类型。 想将userinfo文件中的数据加载到Properties对象当中。 */ // 新建一个输入流对象 FileReader reader = new FileReader("chapter23/userinfo.properties"); // 新建一个Map集合 Properties pro = new Properties(); // 调用Properties对象的load方法将文件中的数据加载到Map集合中。 pro.load(reader); // 文件中的数据顺着管道加载到Map集合中,其中等号=左边做key,右边做value // 通过key来获取value呢? String username = pro.getProperty("username"); System.out.println(username); String password = pro.getProperty("password"); System.out.println(password); } }
配置文件
username=admin
password=456456
具体这部分知识可看我之前的文章
java之序列化与反序列化的详细解析(全)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。