Java容器分为Collection和Map两大类,其下又有很多子类.
Collection还有父接口Iterable,但Iterable接口不算严格意义上的集合的根接口。它称为迭代器,是用于遍历集合元素的一个工具接口。
所以集合的根接口为Collection接口和Map接口,位于java.util包中。
Collection接口
该接口有两个核心子接口:List和Set。
这两个接口都可以保存一组元素,List接口保存元素时,是有序可重复的;Set接口保存元素时,是无序不重复的。
常用方法 | 返回值 | 作用 |
---|---|---|
add(Object obj) | boolean | 将元素添加到集合中 |
size() | int | 获取集合中元素的数量 |
isEmpty() | boolean | 判断集合是否为空 |
clear() | void | 清空集合 |
contains(Object obj) | boolean | 判断集合中是否存在指定元素 |
remove(Object obj) | boolean | 移除集合中的指定元素 |
toArray() | Object[] | 将集合转换为数组 |
iterator() | Iterator | 获取集合的迭代器对象,用于遍历集合 |
List接口(有序可重复)
有序集合,元素可以重复,允许保存null,可以通过索引获取对应位置上的元素。
在该接口继承Collection接口的同时,又拓展了一些操作元素的方法,如添加到指定索引、根据索引删除、获取指定索引的元素、截取子集合的方法等。
常用方法 | 返回值 | 作用 |
---|---|---|
get(int index) | Object | 根据指定索引获取对应的元素 |
set(int index,Object obj) | Object | 使用obj替换index上的元素,返回被替换的元素 |
add(int index,Object obj) | void | 将obj添加到index上 |
remove(int index) | Object | 移除指定索引的元素 |
indexOf(Object obj) | int | 得到某元素第一次出现的索引,没有返回-1 |
lastIndexOf(Object obj) | int | 得到某元素最后一次出现的索引,没有返回-1 |
subList(int from,int to) | list | 截取[from,to)区间内的元素,返回子集合 |
ArrayList实现类(掌握)
采用数组实现的集合
可以通过索引访问元素,可以改变集合大小。如果要在其中插入或删除元素时,会影响后续元素。
该集合中保存的都是引用类型,即便保存了数组123,也保存的是Integer类型的123,而不是int类型的123。
该集合查询效率高,中途增加和删除元素效率低。
构造方法
常用构造方法 | 说明 |
---|---|
ArrayList() | 创建一个Object类型的空数组。在调用添加方法后,才会更改该数组 大小为10 |
ArrayList(int initialCapacity) | 创建一个指定容量的Object数组,如果参数为负,会抛出 IllegalArgumentException异常 |
常用方法
ArrayList中的常用方法,就是Collection接口和List接口中定义的方法。
LinkedList实现类
采用双向链表实现的集合
集合中保存的每个元素也称为节点,除首尾节点外,其余节点都保存了自己的信息外,还保存了其前一个和后一个节点的地址。
如果在双向链表的数据结构中插入和删除操作节点时,不会影响其他节点的位置。如添加时新节点时,只需要重写定义新节点的前后节点位置即可。
如果要查询某个节点时,需要从头结点或尾结点开始一步步得到目标节点的位置。
双向链表在中间插入和删除的效率高,随机读取的效率低。
构造方法
常用构造方法 | 说明 |
---|---|
LinkedList() | 创建一个空链表 |
常用方法
由于LinkedList既实现了List接口,又实现了Deque接口,所以还有Deque接口中的一些方法:
实现Deque接口的方法 | 说明 |
---|---|
addFirst(Object obj) | 添加元素 |
addLast(Object obj) | 添加尾元素 |
removeFirst() | 移除首元素 |
removeLast() | 移除尾元素 |
getFirst() | 得到头元素 |
getLast() | 得到尾元素 |
remove() | 移除首元素 |
pop() | 移除首元素 |
push(Object obj) | 添加首元素 |
peek() | 得到首元素 |
poll() | 移除首元素 |
offer(Object obj) | 添加尾元素 |
ArrayList和LinkedList的区别
这两个类都是List接口的实现类,保存的元素有序可重复,允许保存null。
ArrayList采用数组实现,随机读取效率高,插入删除效率低,适合用于查询。
LinkedList采用双向链表实现,插入删除时不影响其他元素,效率高,随机读取效率低,适合用于频繁更新集合。
//ArrayList
//新闻类
package User;
import java.text.SimpleDateFormat;
import java.util.Date;
public class News {
private String title;
private String content;
private String edit;
public News(String title, String content, String edit) {
this.title = title;
this.content = content;
this.edit = edit;
}
@Override
public String toString() {
String patten = "yyy/MM/dd ";
SimpleDateFormat sdf = new SimpleDateFormat(patten);
Date now = new Date();
String format = sdf.format(now);
return "新闻{" +
"标题='" + title + '\'' +
", 内容='" + content + '\'' +
", 编辑='" + edit + '\'' +
",发布时间='" + format + '\'' +
'}';
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getEdit() {
return edit;
}
public void setEdit(String edit) {
this.edit = edit;
}
}
//新闻管理类
package User;
import java.util.ArrayList;
import java.util.Scanner;
public class Admin {
ArrayList<News> list = new ArrayList<>();
public void add(News news) {
System.out.println("添加成功");
list.add(news);
}
public void Show() {
int i = 1;
for (News news : list) {
System.out.println("编号\t标题");
System.out.println(i++ + "\t" + news.getTitle());
}
}
public void showAll() {
/* String patten = "yyy/MM/dd ";
SimpleDateFormat sdf = new SimpleDateFormat(patten);
Date now = new Date();
String format = sdf.format(now);*/
int i = 1;
for (News news : list) {
//System.out.println("编号\t标题\t内容\t编辑\t发布时间");
System.out.println(i++ + "\t" + news.toString() + "\t");
}
}
public void delete(int i) {
if (i > 0 && i <= list.size()) {
list.remove(i - 1);
} else {
System.out.println("删除失败");
}
}
public void update(int i) {
Scanner sc = new Scanner(System.in);
if (i > 0 && i <= list.size()) {
System.out.println("请输入修改的内容");
News news = list.get(i - 1);
news.setContent(sc.next());
list.set(i - 1, news);
System.out.println("修改成功");
} else {
System.out.println("修改失败");
}
}
}
//Main
package User;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Admin admin = new Admin();
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("欢迎进入新闻管理系统");
System.out.println("请选择功能:");
System.out.println("1.添加新闻");
System.out.println("2.查看所有新闻");
System.out.println("3.查看新闻详情");
System.out.println("4.删除新闻");
System.out.println("5.修改新闻");
System.out.println("6.退出");
switch (sc.nextInt()) {
case 1:
System.out.println("标题");
String q = sc.next();
System.out.println("内容");
String w = sc.next();
System.out.println("编辑");
String e = sc.next();
admin.add(new News(q, w, e));
break;
case 2:
admin.Show();
break;
case 3:
admin.showAll();
break;
case 4:
System.out.println("请输入需要删除新闻的编号");
admin.delete(sc.nextInt());
break;
case 5:
System.out.println("请输入需要修改新闻的编号");
admin.update(sc.nextInt());
break;
case 6:
System.out.println("退出系统");
return;
}
}
}
}
//LinkList
package LinkList;
import java.util.LinkedList;
public class Test {
public static void main(String[] args) {
LinkedList<String> nameList = new LinkedList();
nameList.add("梅西");
nameList.add("C.罗纳尔多");
nameList.add("凯恩");
nameList.add("德布劳内");
nameList.add("内马尔");
//1.在C.罗纳尔多之前添加莱万多夫斯基
int index = nameList.indexOf("C.罗纳尔多");
nameList.add(index,"莱万多夫斯基");
nameList.add(nameList.indexOf("凯恩")+1,"莫德里奇");
nameList.addLast("姆巴顿");
nameList.set(nameList.indexOf("凯恩"),"格列兹曼");
nameList.remove("德布劳内");
System.out.println(nameList);
}
}
Set接口(无序不重复)
无序集合,元素不可以重复,允许保存null,没有索引。
Set接口中没有自己定义的方法,都是继承于Collection接口中的方法。
哈希表hash table
哈希表,也称为散列表,是一种数据结构,能更快地访问数据。
要保存的数据称为原始值,这个原始值通过一个函数得到一个新的数据,这个函数称为哈希函数,这个新数据称为哈希码,哈希码和原始值之间有一个映射关系,这个关系称为哈希映射,可以构造一张映射表,这个表称为哈希表。在哈希表中,可以通过哈希码快速地访问对应的原始值。
假设原本的数据为左侧的数组。
如果要查询10,需要遍历数组,效率不高。
通过一个特定的函数"原始值%5",得到一组新数据,让新数据重新对应元素,保存到“新数组”中,这个“新数组”称为哈希表。
这时如果要查询10,由于哈希函数是通过%5得到了0,所以直接查询哈希表中0对应的元素即可。
整个过程中,这个函数称为哈希函数,得到的新数据称为哈希码,新数组称为哈希表,对应关系称为哈希映射。
这个哈希函数,有一定的几率让多个原始值得到相同的哈希码,这种情况称为哈希冲突(哈希码一致,实际值不同),为了解决哈希冲突,可以使用"拉链法",将2这个哈希码所在的位置向链表一样进行延伸。
哈希码的特点
如果两个对象的hashCode不同,这两个对象一定不同。
如果两个对象的hashCode相同,这两个对象不一定相同。
-
hashCode相同,对象不同,这种现象称为哈希冲突。
-
"通话"和"重地"这两个字符串的hashCode相同,但是两个不同的对象。
HashSet实现类
采用哈希表实现。
元素不能重复,无序保存,允许保存一个null。
本质是一个HashMap对象。
使用HashSet集合时,通常要重写实体类中的equals和hashcode方法。
构造方法
常用构造方法 | 说明 |
---|---|
Hashset() | 创建一个空集合,实际是创建一个HashMap对象。 |
常用方法
HashSet中没有属于自定义的方法,都是重写了父接口Set和Collection中的方法。这里参考Collection中的方法即可。
没有与索引相关的方法。
HashSet添加数据的原理
如果两个元素的hashCode相同且equals结果为true,视为同一个对象,不能添加。
每次向集合中添加元素时,先判断该元素的hashCode是否存在。
-
如果不存在,视为不同对象,直接添加。
-
如果存在,再判断equals方法的结果。
-
如果false,视为不同对象,可以添加。
-
如果true,视为同一对象,不能添加。
-
由此可见,不能添加的条件是两个对象的hashCode相同且equals的结果为true。
如果每次只判断equals的话,由于equals方法通常重写时会判断很多属性,效率不高。
如果每次只判断hashCode的话,效率高,但有可能会有哈希冲突,所以先判断hashCode,再判断equals,技能保证效率,又能保证不添加重复元素。
equals方法和hashCode的关系
-
如果两个对象的equals方法结果为true,在没有重写equals方法的前提下,hashCode相同吗?
- 如果 没有重写equals,默认是Object中使用==判断,如果结果为true,说明是同一个对象,hashCode一定相同
-
如果两个对象的hashCode不同,在没有重写equals方法的前提下,equals方法的结果为?
- hashCode不同,说明不是同一个对象,没有重写equals,说明使用Object中equals的==判断,结果为false
-
如果两个对象的hashCode相同,equals方法的比较结果为?
- 可能为true也可能为false
String str1="hello";
String str2="hello";
//以上两个字符串使用同一个地址,hashCode相同,equals方法为true
String str3="通话";
String str4="重地";
//以上连个字符串是不同地址,但hashCode相同,因为哈希冲突,equals方法为false
HashSet的应用
如果想要保存的对象保证不重复,且无关顺序,可以使用HashSet。如学生管理
Goods类
//Goods类
package hashSet;
import java.util.Objects;
public class Goods {
private String brand;
private String name;
private int price;
public Goods(String brand, String name, int price) {
this.brand = brand;
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "Goods{" +
"brand='" + brand + '\'' +
", name='" + name + '\'' +
", price=" + price +
'}';
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Goods goods = (Goods) o;
return price == goods.price &&
brand.equals(goods.brand) &&
name.equals(goods.name);
}
@Override
public int hashCode() {
return Objects.hash(brand, name, price);
}
}
//Main
package hashSet;
import java.util.HashSet;
public class Main {
public static void main(String[] args) {
HashSet<Goods> hs = new HashSet<>();
Goods g1 = new Goods("康师傅", "冰红茶", 3);
Goods g2 = new Goods("康师傅", "泡面", 4);
Goods g3 = new Goods("农夫山泉", "矿物质水", 2);
Goods g4 = new Goods("农夫山泉", "矿物质水", 2);
hs.add(g1);
hs.add(g2);
hs.add(g4);
hs.add(g3);
for (Goods g : hs) {
System.out.println(g);
}
}
}
TreeSet实现类
-
特殊的Set实现类,数据可以有序保存,可以重复,不能添加null。
-
采用红黑树(自平衡二叉树)实现的集合。
-
二叉树表示某个节点最多有两个子节点。
-
某个节点右侧节点值都大于左侧节点值。
-
红黑树会经过不停的"变色"、"旋转"达到二叉树的平衡。
-
-
只能添加同一种类型的对象且该类实现了Comparable接口。
-
实现Comparable接口后必须要重写compareTo()方法。
-
每次调用添加add(Object obj)方法时,就会自动调用参数的compareTo()方法。
-
-
compareTo()方法的返回值决定了能否添加新元素和新元素的位置。
-
如果返回0,视为每次添加的是同一个元素,不能重复添加。
-
如果返回正数,将新元素添加到现有元素之后。
-
如果返回负数,将新元素添加到现有元素之前。
-
-
添加的元素可以自动排序。
构造方法
常用构造方法 | 说明 |
---|---|
TreeSet() | 创建一个空集合,实际是创建了一个TreeMap对象 |
常用方法
属于Set的实现类,所以能使用Collection和Set中的方法,除此之外,还有独有的方法
常用方法 | 作用 |
---|---|
fisrt() | 得到集合中的第一个元素 |
last() | 得到集合中的最后一个元素 |
ceil(Object obj) | 得到比指定元素obj大的元素中的最小元素 |
floor(Object obj) | 得到比指定元素obj小的元素中的最大元素 |
TreeSet的应用
如果要保存的元素需要对其排序,使用该集合。
保存在其中的元素必须要实现Comparable接口,且重写compareTo()方法,自定义排序规则
Employee类
//Employ类
public class Employee implements Comparable{
private int no;
private String name;
private String dept;
public Employee(int no, String name, String dept) {
this.no = no;
this.name = name;
this.dept = dept;
}
@Override
public String toString() {
return "Employee{" +
"no='" + no + '\'' +
", name='" + name + '\'' +
", dept='" + dept + '\'' +
'}';
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDept() {
return dept;
}
public void setDept(String dept) {
this.dept = dept;
}
@Override
public int compareTo(Object o) {
Employee emp = (Employee) o;
return this.getNo()-emp.getNo();
}
}
//Main类
import java.util.TreeSet;
public class Main {
public static void main(String[] args) {
TreeSet<Employee> emps = new TreeSet<>();
Employee e1 = new Employee(10023,"aaa","市场部");
Employee e2 = new Employee(10025,"ccc","市场部");
Employee e3 = new Employee(10028,"bbb","市场部");
Employee e4 = new Employee(10028,"xxx","市场部");
//第一个元素直接添加
emps.add(e1);
//第二个元素添加时,调用compareTo()方法 e2.compareTo(e1) e2.10025 -
e1.10023 结果为正,添加在现有元素之后
emps.add(e2);
emps.add(e3);
//添加该元素时,调用compareTo()方法 e4.10028 - e3.10028 结果为0,不添加
emps.add(e4);
for (Employee emp : emps) {
System.out.println(emp);
}
}
}
Map接口
Map称为映射,数据以键值对的形式保存。保存的是键与值的对应关系。
键称为Key,值称为Value,键不能重复,键允许出现一个null作为键,值无限制。
键和值都是引用类型。
如,yyds就是一个键key,代表了一个含义:“永远单身”即为值value。
常用方法 | 作用 |
---|---|
size() | 得到键值对的数量 |
clear() | 清空所有键值对 |
put(Object key,Object value) | 向集合中添加一组键值对 |
get(Object key) | 在集合中根据键得到对应的值 |
remove(Object key)/remove(Object key,Object key) | 根据键或键值对移除 |
keyset() | 获取键的集合 |
values | 获取值的集合 |
containsKey(Object key) | 判断是否存在某个键 |
containsValue(Object value) | 判断是否存在某个值 |
entrySet() | 得到键值对的集合 |
HashMap实现类
JDK1.8之后,HashMap采用"数组+链表+红黑树"实现。
-
当没有哈希冲突时,元素保存到数组中。
-
如果出现哈希冲突,在对应的位置上创建链表,元素保存到链表中。
-
如果链表的长度大于8,将链表转换为红黑树。
数据采用键值对key-value的形式保存,键不能重复,能用null作为键;值没有限制,键和值都是引用类型。
向HashMap集合中添加元素时,原理同HashSet。
构造方法
常用构造方法 | 作用 |
---|---|
HashMap() | 创建一个空的映射集合,默认大小为16,加载因子为0.75 |
常用方法
常用方法参考Map中的方法。
Map的应用
//people类
package HashMap;
import java.util.Objects;
public class People {
private String username;
private String password;;
public People(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
//Main
package HashMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
Map<Integer,People> map= new HashMap();
People p1=new People("xx","wx123");
People p2=new People("zz","wx123");
People p3=new People("qq","wx123");
map.put(1001, p1);
map.put(1002, p2);
map.put(1003, p3);
System.out.println("编号\t\t用户信息\t\t密码");
for (Integer id:map.keySet()){
System.out.println(id+"\t"+map.get(id).getUsername()+"\t\t\t"+map.get(id).getPassword());
}
}
}
遍历集合中元素的方式
遍历List集合
ArrayList<String> nameList = new ArrayList();
nameList.add("Tom");
nameList.add("Jerry");
nameList.add("LiHua");
nameList.add("Danny");
方式一:普通for循环
System.out.println("使用普通for循环遍历");
//方式一:普通for循环
for (int i = 0; i < nameList.size(); i++) {//从0遍历到size()
String name = nameList.get(i);//通过get(int index)获取指定索引的元素
System.out.println(name);
}
方式二:增强for循环
System.out.println("使用增强for循环遍历");
//方式二:增强for循环
for (String name : nameList) {
System.out.println(name);
}
方式三:迭代器
System.out.println("使用迭代器遍历");
//方式三:迭代器
//Collection类型的集合对象.iterator(),获取迭代器
Iterator<String> iterator = nameList.iterator();
// iterator.hasNext()判断集合中是否还有下一个元素
// iterator.next();获取下一个元素
while (iterator.hasNext()) {
String name = iterator.next();
System.out.println(name);
}
遍历Set集合
Set hs = new HashSet();
hs.add(123);
hs.add("hello");
hs.add(null);
hs.add(987);
方式一:增强for循环
for(Object o : hs){
System.out.println(o);
}
方式二:迭代器
Iterator<Object> it = hs.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
遍历Map集合
Map<Integer, User> hm = new HashMap<>();
User u1 = new User("admin", "123123");
User u2 = new User("tom", "123123");
User u3 = new User("jerry", "123123");
hm.put(1001, u1);
hm.put(1002, u2);
hm.put(1003, u3);
//遍历hashMap
for (Integer id : hm.keySet()) {//遍历键
//根据键得到对应的值
System.out.println(id + "\t" + hm.get(id).getUsername());
}
//得到当前hashmap对象中的所有键值对的集合
Set<Map.Entry<Integer, User>> entries = hm.entrySet();
//遍历键值对
for (Map.Entry<Integer, User> entry : entries) {
System.out.println(entry);
}
泛型
一种规范,常用于限制集合中元素的类型,省去遍历元素时判断是否为对应类型和转型的过程:
//集合在定义后,默认可以添加任意类型的数据,但通常情况下,都是保存同一种类型
List list = new ArrayList();
list.add(123);
list.add(null);
list.add("hello");
//这时如果没有限制类型,使用增强for循环遍历集合中的元素时,就只能使用Object类型变量接收
for(Object o : list){
}
用法
在定义集合遍历时,在类后面写上<引用数据类型>
集合类或接口<引用数据类型> 集合变量名 = new 集合实现类();
List<String> list = new ArrayList();
//当前集合只能保存String类型的元素
list.add("sdfsdf");
//list.add(123);//无法添加
List<Integer> list2 = new ArrayList();
list2.add(123);
Collections集合工具类
Collection是集合的根接口,定义了集合操作元素的方法
Collections是集合的工具类,定义了集合操作元素的静态方法
常用方法
集合和数组之间的转换
集合转换为数组:使用Collection接口中的toArray()方法
Object[] obj = 集合对象.toArray();
List<Integer> list = new ArrayList();
list.add(123);
list.add(63);
list.add(3);
Integer[] nums =(Integer[]) list.toArray();
数组转换为集合
//一个数组对象
int[] nums ={11,2,66,3,6,21};
//定义集合对象
List list = new ArrayList();
//遍历数组的同时添加到集合中
for(int i:nums){
list.add(i);
}
一组数据转换为集合:使用Arrays工具类中的asList(一组数据)方法
//通常将数组中的数据直接作为参数
List<String> strings = Arrays.asList("XX", "aa", "qq", "xx");
import java.util.ArrayList;
import java.util.Random;
public class Test {
public static void main(String[] args) {
String[] huaSe = {"♥", "♠", "♣", "♦"};
String[] num = {"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"};
ArrayList<String> paiKu = new ArrayList();
//循环嵌套构造牌添加到牌库中
for (String s : huaSe) {
for (String s1 : num) {
paiKu.add(s + s1);
}
}
//额外添加大小王
paiKu.add("JOKER");
paiKu.add("joker");
//可以通过集合的工具类Collections调用静态方法shuffle()将集合打乱元素顺序
// Collections.shuffle(paiKu);
// System.out.println(paiKu);
ArrayList<String> player1 = new ArrayList<>();
ArrayList<String> player2 = new ArrayList<>();
ArrayList<String> player3 = new ArrayList<>();
ArrayList<String> diPai = new ArrayList<>();
Random rd = new Random();
//循环54次
for (int i = 0; i < 54; i++) {
//前17张
if (i < 17) {
//根据当前牌库数量得到一个随机数
int index = rd.nextInt(paiKu.size());
//得到对应的牌
String s = paiKu.get(index);
//添加给第一个玩家
player1.add(s);
//移除这张牌
paiKu.remove(s);
} else if (i < 34) {
String s = paiKu.get(rd.nextInt(paiKu.size()));
player2.add(s);
paiKu.remove(s);
} else if (i < 51) {
String s = paiKu.get(rd.nextInt(paiKu.size()));
player3.add(s);
paiKu.remove(s);
} else {
String s = paiKu.get(rd.nextInt(paiKu.size()));
diPai.add(s);
paiKu.remove(s);
}
}
System.out.println("玩家1"+player1);
System.out.println("玩家2"+player2);
System.out.println("玩家3"+player3);
System.out.println("地主牌"+diPai);
}
}
1. Java容器有哪些?
2. Collection 和 Collections 有什么区别?
- Collection是一个接口,Collections是集合类的一个工具类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索、以及线程安全等各种操作。是一个接口,Collections是集合类的一个工具类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索、以及线程安全等各种操作。
3.List、Set、Map之间区别是什么?
- List存储的元素是有序可重复的,Set存储的元素是无序不可重复的,Map使用键值对存储,key是无序的、不可重复的,value是无序的、可重复的。
4. HashMap 和 Hashtable 有什么区别?
- HashMap是非线程安全的,HashTable是线程安全的。因为线程安全的问题,所以HashMap要比HashTable效率高一点,HashMap可以存储null的key和value而HashTable不可以,否则会抛出空指针异常。HashTable初始容量为11,之后扩容是2n+1。
5. 如何决定使用HashMap还是TreeMap?
-
HashMap基于散列桶(数组和链表)实现;TreeMap基于红黑树实现。
-
HashMap不支持排序;TreeMap默认是按照Key值升序排序的,可指定排序的比较器,主要用于存入元素时对元素进行自动排序。
-
HashMap大多数情况下有更好的性能,尤其是读数据。在没有排序要求的情况下,使用HashMap。
6. 说一下 HashMap 的实现原理?
- jdk8以前,例如jdk7底层创建了长度是16的一维数组Entry[] table ,map.put()方法首先调用key所在类的hashCode()方法计算key的哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置,如果此位置上数据为空,此时key value添加成功,如果此位置上数据不为空,意味着此位置上存在一个或多个数据(以链表形式存在),比较当前key和已经存在的一个或多个数据的哈希值,如果key的哈希值与已经存在的数据的哈希值都不相同,此时key value添加成功。如果key的哈希值和已经存在的某一个数据的哈希值相同,继续调用key所在类的equals()方法,比较,如果equals返回false,key value添加成功。如果返回true,使用value替换相同key的value值。在不断的添加过程中,会涉及到扩容问题,当某一索引位置上数据个数>8且数组长度<64会扩容(前提是当前数组长度不为空,为空则首先会扩容成16),当存放元素个数超出临界值(size*0.75)且要存放的位置非空时,扩容。默认扩容为原来容量的2倍,并将原有的数据复制过来。jdk8底层没有创建一个长度为16的数组,jdk8底层的数组是Node[],而非Entry[],首次调用put()方法时,底层创建长度为16的数组,jdk7底层结构只有数组+链表,jdk8底层结构是数组+链表+红黑树。当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时索引位置上的所有数据改为使用红黑树存储。
7、说一下 HashSet 的实现原理?
- HashSet是基于HashMap 实现的,HashSet 底层使用HashMap来保存所 有元素,因此HashSet的实现比较简单, 相关HashSet 的操作, 基本上都是 直接调用底层HashMap的相关方法来完成, HashSet不允许重复的值。
8.ArrayList 和 LinkedList 的区别是什么?
相同点:都是List接口下的实现类,都是单列集合;都是线程不安全的。
不同点:
-
数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表(凡是出现linked都是基于双向链表的)的数据结构实现。数组大小固定,不能动态拓展。 内存空间要求高,必须有足够的连续内存空间。数组可能浪费内存空间,容量存满的时候再添加元素需要扩充,没存满又造成部分内存空间的浪费;而链表不存在长度固定,也不需要扩容,内存空间利用率高,但是每个元素都有前驱节点和后驱节点的空间消耗。
-
随机访问效率:对于随机访问的get(index)和set(index),ArrayList要优于LinkedList,因为LinkedList要移动指针.而数组可以直接通过数组名【index】获取或者设置元素。
-
增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 中间和头部增删,会涉及数组元素位置的移动,效率比较低。
-
内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素,数据量大的话会占用不少内存空间。
9.如何实现数组和 List 之间的转换?
- List转成为数组,调用ArrayList.toArray()方法,数组转化为list,调用Arrays.asList()方法。
10.Array 和 ArrayList 有何区别?
Array:
-
数组(Array)是有序的元素序列。若将有限个类型相同的变量的集合命名,那么这个名称为数组名。组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量。用于区分数组的各个元素的数字编号称为下标。数组是在程序设计中,为了处理方便, 把具有相同类型的若干元素按有序的形式组织起来的一种形式。这些有序排列的同类数据元素的集合称为数组。
-
数组是用于储存多个相同类型数据的集合
ArrayList:
- ArrayList就是动态数组,用MSDN中的说法,就是Array的复杂版本,它提供了动态的增加和减少元素,实现了ICollection和IList接口,灵活的设置数组的大小等好处
11.哪些集合类是线程安全的?
- 线程安全类是确保类的内部状态以及从方法返回的值在从多个线程并发调用时是正确的类。 Java中线程安全的集合类有Stack、Vector、Properties、Hashtable等。
12. 迭代器 Iterator 是什么?
-
Iterator 是可以遍历集合的对象,为各种容器提供了公共的操作接口,隔离对容器的遍历操作和底层实现(解耦)。
-
缺点是增加新的集合类需要对应增加新的迭代器类,迭代器类与集合类也成对增加。
13. Iterator 怎么使用?有什么特点?
Iterator的使用
-
Iterator()要求容器返回一个Iterator。Iterator将准备好返回序列的第一个元素。
-
使用next()获得序列中的下一个元素
-
使用hasNext()检查序列中是否还有元素。
-
使用remove()将迭代器新近返回的元素删除。
Iterator的特点
-
Iterator遍历集合元素的过程中不允许线程对集合元素进行修改,否则会抛出ConcurrentModificationEception的异常。
-
Iterator遍历集合元素的过程中可以通过remove方法来移除集合中的元素,删除的是上一次Iterator.next()方法返回的对象。
-
Iterator必须依附于一个集合类对象而存在,Iterator本身不具有装载数据对象的功能。
-
next()方法,该方法通过游标指向的形式返回Iterator下一个元素。
14.Iterator和ListIterator的区别是什么?
-
ListIterator 只能对 List 迭代,而 Iterator 不仅可以对 List 迭代,还可以迭代Set。
-
ListIterator 可以双向迭代,而 Iterator 只能单向迭代。
-
ListIterator 继承 Iterator 接口增加了更多的方法。
15.怎么确保一个集合不能被修改?
- 可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。
List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());
16.线程和进程的区别?
-
进程是正在运行程序的实例,进程中包含了线程,而线程中不能包含进程。
-
进程是操作系统分配资源的基本单位,而线程是操作系统调度的基本单位。
-
多个进程间不能共享资源,每个进程有自己的堆、栈、虚存空间(页表)、文件描述符等信息,而线程可以共享进程资源文件(堆和方法区)。
-
线程上下文切换速度很快(上下文切换指的是从一个线程切换到另一个线程),而进程的上下文切换的速度比较慢。
-
一般情况下进程的操纵者是操作系统,而线程的操纵者是编程人员。
17.创建线程有哪几种方式?有什么区别?
采用继承Thread类方式:
-
优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
-
缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。
采用实现Runnable接口方式:
-
优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
-
缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
18. 说一下 runnable 和 callable 有什么区别?
相同点:
-
两者都是接口。
-
两者都需要调用Thread.start启动线程。
不同点:
-
如上面代码所示,callable的核心是call方法,允许返回值,runnable的核心是run方法,没有返回值。
-
call方法可以抛出异常,但是run方法不行。
-
因为runnable是java1.1就有了,所以他不存在返回值,后期在java1.5进行了优化,就出现了callable,就有了返回值和抛异常。
-
callable和runnable都可以应用于executors。而thread类只支持runnable。
19.在 Java 程序中怎么保证多线程的运行安全?
-
方法一:使用安全类,比如 Java. util. concurrent 下的类。
-
方法二:使用自动锁 synchronized。
-
方法三:使用手动锁 Lock。
20.什么是死锁?
- 死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
21. 产生死锁的条件?
-
互斥使用:一个资源被一个线程使用时,另一个线程不可以使用。
-
不可抢占:资源的请求者不能强行从资源占有者手里夺走资源。
-
占有等待:当资源请求者请求其他资源的同时,保持对象有资源的占有。
-
循环等待:线程1 等待 线程2 占有的资源,线程2 等待 线程1 占有的资源,形成一个环路。
22.怎么防止死锁?
死锁预防,那就是要破坏这四个必要条件:
-
由于资源互斥是资源使用的固有特性,无法改变,所以我们不讨论。
-
破坏不可剥夺条件:一个线程不能获得所需要的全部资源时便处于等待状态,等待期间它占有的资源将被隐式的释放,重新加入到系统的资源列表中,可以被其他的线程使用,而等待的线程只有重新获得自己原有的资源以及新申请的资源时才可以重新启动执行。
-
破坏请求与保持:
-
第一种方式静态分配即每个线程在开始执行时就申请它所需的全部资源。
-
第二种是动态分配即每个线程在申请所需要的资源时它本身不占用系统资源。
-
-
破坏循环等待条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的、稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个线程只有获得了较小的编号才能申请较大的编号。
23.synchronized 和 Lock 有什么区别?
-
lock是一个接口,而synchronized是java的一个关键字。
-
synchronized在发生异常时会自动释放占有的锁,因此不会出现死锁;而lock发生异常时,不会主动释放占有的锁,必须手动来释放锁,可能引起死锁的发生。
24.sleep() 和 wait() 有什么区别?
-
相同点
- sleep()和wait()都可以暂停线程的执行。
-
不同点 (所在类不同)
-
sleep()是Thread类的静态方法。
-
wait()是Object类的方法
-
25.Java中的四种引用?
- Java中有四种引用类型:强引用、软引用、弱引用、虚引用。
26.什么是 Java 序列化?什么情况下需要序列化?
什么是序列化:
-
序列化 (Serialization) 是一种用来处理对象流的机制,即将对象写入到 IO 流中。
- 所谓对象流就是将对象的内容进行流化,可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间;序列化是为了解决在对象流进行读写操作时所引发的问题。序列化的思想是“冻结”对象状态,然后写到磁盘或者在网络中传输;反序列化的思想是“解冻”对象状态,重新获得可用的 Java 对象。
序列化的实现:
-
将需要被序列化的类实现 Serializable 接口,该接口没有需要实现的方法,implements Serializable 只是为了标注该对象是可被序列化的。
-
然后使用一个输出流(如:FileOutputStream )来构造一个ObjectOutputStream (对象流)对象。
-
接着,使用 ObjectOutputStream 对象的 writeObject(Object obj) 方法就可以将参数为 obj 的对象写出(即保存其状态),要恢复的话则用输入流即可。
什么时候使用序列化
-
对象序列化可以实现分布式对象。主要应用例如:RMI (remote method invoke,即远程方法调用)要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。
-
Java 对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的"深复制",即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。