一. Java集合框架概述
一方面,面向对象语言,所有事物的体现都是以对象的方式,而要操作多个对象,就要对对象进行存储。另一方面,使用数组Array存储对象具有一些弊端,(为什么?)而java集合就像一种容器,可以动态地把多个对象的引用放入容器中。
数组的特点及弊端:
①内存方面,数组初始化后,长度确定;数组声明的类型,决定了进行元素初始化时的类型。
②存储方面,数组初始化后长度不可变,不便于扩展;数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高,同时无法直接获取存储元素的个数;数组存储的数据是有序且可重复的,其存储数据的特点比较单一。
- java集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组。
集合的使用场景:
Android客户端:将json对象或json数组转换为java对象或java对象构成的List;
服务器端:将java对象的或java对象构成的List转换为json对象或json数组;
数据库:
集合框架涉及到的API
两大体系:Collection 和 Map两种体系
Collection接口:单列数据,定义了存取一组对象的方法的集合
List:元素有序、可重复的集合 (重点)
Set:元素无序、不可重复的集合
Map接口:双列数据,保存具有映射关系”key-value对“的集合 (重点)
二. 集合框架
* |----Collection接口:单列集合,存储一个一个的对象。存储int,short等使用包装类
* |----List接口:存储有序的、可重复的数据 -->”动态“数组
* |----ArrayList、LinkedList、Vector
* |----Set接口:存储无序的、不可重复的数据 -->类似高中讲的”集合“
* |----HashSet、LinkedHashSet、TreeSet
* |----Map接口:双列集合,用来存储一对(key-value)一对的数据 -->高中函数:y = f(x)
* |----HashMap、LinkedHashMap、TreeMap、Hashtable、Properties
二. Collection接口常用方法1
@Test
public void test1(){
Collection coll = new ArrayList();
//1. add(Object e): 将元素e听啊加到集合coll中
coll.add("AA");
coll.add("BB");
coll.add(123);
coll.add(new Date());
//2. size(): 获取添加的元素个数
System.out.println(coll.size()); // 4
//3. addAll(Collection coll1):将coll1集合中的元素添加到当前的集合中
Collection coll1 = new ArrayList();
coll1.add(456);
coll1.add("CC");
coll.addAll(coll1);
System.out.println(coll.size()); // 6
// 4. clear():清空集合元素
coll.clear();
//5. isEmpty():判断当前集合是否为空
System.out.println(coll.isEmpty()); // true
}
三. 复习
- 什么是枚举类?枚举类的对象声明的修饰符有哪些?
枚举类:类中的对象的个数是确定的,有限个。枚举类中只有一个时,就类似单例。
public static final
- 什么是元注解?说说Retention和Target元注解的作用
元注解:对注解的注解,(对现有注解进行解释说明的注解)
Retention:指明所修饰的注解的生命周期。
生命周期:SOURCE、CLASS、RUNTIME
- 说说你理解的集合框架都有哪些接口,存储数据的特点是什么?
见上。
- 比较throw和throws的异同
同:其实没什么关系,都在异常阶段涉及到
不同:
throw:生成一个异常对象,并抛出。使用时在方法内部<->自动抛出异常对象
throws:相当于处理异常的方式。使用在方法声明处的末尾<->try-catch-finally
两者相当于递进关系,”上游排污,下游治污“
- 谈谈对同步代码块中同步监视器和共享数据的理解及各自要求
同步监视器: 俗称锁。①任何一个类的对象都可以充当锁。②多个线程共用一把锁。
共享数据:多个线程共同操作的数据,即为共享数据。
需要使用同步机制将操作共享数据的代码包起来。不能包多了,也不能包少了。
1. 枚举类
- 枚举类的说明
①枚举类的理解:类的对象只有有限个,去欸的那个的,我们称此类为枚举类
②当需要定义一组常量时,建议使用枚举类。
③如果枚举类中只有一个对象,则可以作为单例模式的实现方式
- 如何自定义枚举类?步骤:
//自定义枚举类
class Season{
//1. 声明Season对象的属性
private final String seasonName;
private final String seasonDesc;
//2. 私有化类的构造器
private Season(String seasonName, String seasonDesc){
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
//3.提供当前枚举类的多个对象: 声明为public static final的
public static final Season SPRING = new Season("春天", "春暖花开");
public static final Season SUMMER = new Season("夏天", "夏日炎炎");
public static final Season AUTUMN = new Season("秋天", "秋高气爽");
public static final Season WINTER = new Season("冬天", "冬雪皑皑");
//4.其他需求:获取枚举类对象的属性等
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
// toString方法
@Override
public String toString() {
return "Season{" +
"seasonName='" + seasonName + '\'' +
", seasonDesc='" + seasonDesc + '\'' +
'}';
}
}
- jdk5.0之后使用enum关键字定义枚举类:
enum Season1{
//1.提供当前枚举类的对象,多个对象之间用逗号隔开,末尾对象用;结尾
SPRING("春天", "春暖花开"),
SUMMER("夏天", "夏日炎炎"),
AUTUMN("秋天", "秋高气爽"),
WINTER("冬天", "冬雪皑皑");
private final String seasonName;
private final String seasonDesc;
//2. 私有化类的构造器
private Season1(String seasonName, String seasonDesc){
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
//4.其他需求:获取枚举类对象的属性等
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
}
- 使用enum定义枚举类后,枚举类常用方法:(继承于java.lang.Enum类)
public static void main(String[] args) {
Season1 summer = Season1.SUMMER;
System.out.println(summer); // SUMMER
//System.out.println(Season1.class.getSuperclass());
// toString():返回枚举类对象的名称
System.out.println(summer.toString());
// values():返回所有的枚举类对象构成的数组
Season1[] values = Season1.values();
for (int i = 0; i < values.length; i++){
System.out.println(values[i]); // 所有对象名
}
System.out.println("**********************");//遍历输出线程的四个状态
Thread.State[] values1 = Thread.State.values();
for (int i = 0; i < values1.length; i++) {
System.out.println(values1[i]);
}
// NEW RUNNABLE BLOCKED WAITING TIMED_WAITING TERMINATED
// valueOf(String objName):根据提供的objName参数,返回枚举类中对象名是objName的对象
Season1 winter = Season1.valueOf("WINTER");
System.out.println(winter); // WINTER
}
- 使用enum定义枚举类后,如何让枚举类对象分别实现接口
interface Info{
void show();
}
enum Season1 implements Info{
//1.提供当前枚举类的对象,多个对象之间用逗号隔开,末尾对象用;结尾
SPRING("春天", "春暖花开"){
@Override
public void show() {
System.out.println("春天在哪里");
}
},
SUMMER("夏天", "夏日炎炎"){
@Override
public void show() {
System.out.println("宁静的夏天");
}
},
AUTUMN("秋天", "秋高气爽"){
@Override
public void show() {
System.out.println("秋天不回来");
}
},
WINTER("冬天", "冬雪皑皑"){
@Override
public void show() {
System.out.println("大约在冬季");
}
};
2. 注解
- 注解的理解
①jdk5.0新增的功能
②Annotation其实就是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。
通过使用Annotation,程序员可以在不改变原洛基的情况下,在源文件中嵌入一些补充信息。
③ 在javaSE中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在javaSE/Android中注解占据
了更重要的角色,例如用来配置应用程序的任何切面,代替javaSE旧版中所遗留的繁冗代码和XML配置等。
框架 = 注解 + 反射 + 设计模式
- 注解的使用示例
示例一:生成文档相关的注解
示例二;在编译时进行格式检查(JDK内置的三个基本注解)
@Override:限定重写父类方法,该注解只能用于方法
@Deprecated:用于表示所修饰的元素(类、方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择
@SupperssWarnings:抑制编译器警告
示例三:跟踪代码依赖性,实现替代配置文件功能
- 如何自定义注解
自定义注解:
① @interface + 名字
② 内部定义成员,通常用value表示
③ 可以指定成员的默认值,使用default定义
④ 如果自定义的注解没有成员,表明是一个标识作用
如果注解有成员,使用时需要指明成员的值。
自定义注解必须配上注解的信息处理流程(使用反射)才有意义。
自定义注解通常都会指明两个元注解:Retention、Target
参照@SupperssWarnings定义。
@Inherited
@Repeatable(MyAnnotation.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
public @interface MyAnnotation {
String value() default "hello";
}
//@MyAnnotation(value = "hello")
//@MyAnnotation()
@MyAnnotation(value = "hi")
class Teacher{
// ...
}
- 元注解:对现有的注解进行解释说明的注解。
JDK5.0提供了4个标准的meta-annotation类型,分别是:
Retention
Target
Documented
Inherited
- 如何获取注解信息
通过反射来进行获取、调用。
前提:要求此注解的元注解Retention中生命周期状态为:RUNTIME。
- jdk8中注解的新特性
可重复注解、类型注解。
3. Collection
/**
* 一. 集合框架的概述
* 1. 集合、数组都是对多个数据进行存储操作的结构,简称java容器
* 说明:此时的存储,主要指内存层面的存储,不涉及到持久化的存储
* 2.1 数组存储多个数据方面的特点:
* > 一旦初始化,长度就确定了
* > 一大你当一号,元素类型也确定了,只能操作指定类型的数据了。
* 2.2 数组存储多个数据方面的弊端:
* > 初始化后长度不能变。
* > 操作单一,提供的属性方法较少,插入删除不方便,效率低。
* > 获取数组实际存储元素个数,没有现成的属性/方法可用。
* > 数组存储数据的特点:有序的、可重复的,对于无序、不可重复的需求,数组无法满足。
集合的优点:解决数组的弊端
* 二. 集合框架
* |----Collection接口:单列集合,存储一个一个的对象。存储int,short等使用包装类
* |----List接口:存储有序的、可重复的数据 -->”动态“数组
* |----ArrayList、LinkedList、Vector
* |----Set接口:存储无序的、不可重复的数据 -->类似高中讲的”集合“
* |----HashSet、LinkedHashSet、TreeSet
* |----Map接口:双列集合,用来存储一对(key-value)一对的数据 -->高中函数:y = f(x)
* |----HashMap、LinkedHashMap、TreeMap、Hashtable、Properties
*/
四. Collection接口的常用方法2
@Test
public void test1(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(false);
coll.add(new String("Tom"));
coll.add(new Person("Jerry",20));
// 1. contains(Object obj):判断当前集合当中是否包含obj
boolean contains1 = coll.contains(123);
System.out.println(contains1);
//我们在判断时会调用Object类的equals方法来比较。
//此处true,因为String重写了equals,判断的是内容,而不是地址
System.out.println(coll.contains(new String("Tom")));
//此处false,因为是new的对象和存储的对象是两个不同的对象
System.out.println(coll.contains(new Person("Jerry",20)));
// 想要为true,则在Person类中重写equals方法即可。
// 2. containsAll(Collection coll1):判断形参coll1中的所有元素是否都存在于当前集合当中
Collection coll1 = Arrays.asList(123, 456);
System.out.println(coll.containsAll(coll1)); //true
}
向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals方法
五. Collection接口的常用方法3
@Test
public void test2(){
// 3. remove(Object obj):从当前集合中删除obj元素
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(false);
coll.add(new String("Tom"));
coll.add(new Person("Jerry",20));
coll.remove(123); //调用equals先判断有没有元素,再删除
System.out.println(coll);// [456, false, Tom, Person{name='Jerry', age=20}]
coll.remove(new Person("Jerry",20));
System.out.println(coll); // 这里Person若重写了equals,则能删除掉。
// 4. removeAll(Collection coll1):(差集) 从当前集合中移除coll1中的所有元素
Collection coll1 = Arrays.asList(123, 456);
coll.removeAll(coll1);
System.out.println(coll);// [false, Tom, Person{name='Jerry', age=20}]
}
@Test
public void test3(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
//5. equals(Object obj):比较两个集合是否相同 因为是ArrayList所以必须有序+相同对象
Collection coll2 = new ArrayList();
coll2.add(123);
coll2.add(456);
coll2.add(new String("Tom"));
coll2.add(false);
System.out.println(coll.equals(coll2)); //true
//6. retainAll():求交集,获取当前集合和coll1集合的交集,并修改当前集合为交集
Collection coll1 = Arrays.asList(123, 456, 789);
coll.retainAll(coll1);
System.out.println(coll);
}
六. Collection接口的常用方法4
@Test
public void test4(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
//7. hashCode():返回当前对象的哈希值
System.out.println(coll.hashCode());
//8. 集合 --> 数组: toArray()
Object[] arr = coll.toArray();
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
// 拓展: 数组 --> 集合 asList
List<String> list = Arrays.asList(new String[]{"aa","bb"});
System.out.println(list); // [aa, bb]
//注意这里
List arr0 = Arrays.asList(new int[]{123, 456});
System.out.println(arr0); // [[I@3aeaafa6]输出了地址值,将整个int数组当成了一个整体
// 改正①
List arr1 = Arrays.asList(123, 456);
System.out.println(arr1); // [123, 456]
//改正②
List arr2 = Arrays.asList(new Integer[]{123, 456});
System.out.println(arr2); // [123, 456]
// <T> T[]泛型略
//iterator():返回Iterator接口的实例,用于遍历集合元素,放在IteratorTest.java中测试
}
七. 使用Iterator遍历Collection
/**
* 集合元素的遍历,使用迭代器iterator接口
* hasNext()和next()
*/
@Test
public void test1(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
Iterator iterator = coll.iterator();
// System.out.println(iterator.next());
// System.out.println(iterator.next());
// System.out.println(iterator.next());
// System.out.println(iterator.next());
// //4个元素,第5个越界异常
// System.out.println(iterator.next());
//方式二
for (int i = 0; i < coll.size(); i++) {
System.out.println(iterator.next());
}
//方式三 推荐,开发常用
while (iterator.hasNext())
{
System.out.println(iterator.next());
}
}
八. 迭代器Iterator的执行原理
- 调用iterator.next()时①指针下移一位②将下移后集合位置上的元素返回;
- Iterator iterator = coll.iterator(); 其中iterator只是用来迭代,并不是容器;
九. Iterator遍历集合的两种错误写法
@Test
public void test2() {
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
//Iterator iterator = coll.iterator();
//错误方式一 跳着输出+越界异常
// while (iterator.next() != null)
// {
// System.out.println(iterator.next());
// }
//错误方式二 每次从第一个判断,死循环,不断输出第一位
// while (coll.iterator().hasNext())
// {
// System.out.println(coll.iterator().next());
// }
}
方式二原因是:集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
十. Iterator迭代器remove()的使用
移除集合中的元素。
@Test
public void test3() {
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
Iterator iterator = coll.iterator();
while (iterator.hasNext())
{
Object obj = iterator.next();
if ("Tom".equals(obj))
{
iterator.remove();
}
}
//上面遍历完iterator已经走到末尾没有了,想要重新遍历需要再次获取一个iterator对象
iterator = coll.iterator();
while (iterator.hasNext())
{
System.out.println(iterator.next());
}
}
不同于集合中直接调用的remove()方法。
如果①还未调用next()方法或②在上一次调用next方法之后已经调用了remove方法,则调用remove都会报illegalStateException。(①指针指向空;②同一位置不能删两次)
十一. 新特性foreach循环遍历集合或数组
jdk5.0提供,增强for循环。
@Test
public void test1() {
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
// for(集合中元素的类型 局部变量: 集合对象)
for(Object obj: coll){ //每次遍历自动去取集合中的元素,本质也是迭代器
System.out.println(obj);
}
}
@Test
public void test2(){
int[] arr = new int[]{1, 2, 3, 4, 5};
for (int i : arr) {
System.out.println(i);
}
}
@Test
public void test3(){
String[] arr = new String[]{"MM", "MM", "MM", "MM", "MM"};
//方式一
// for (int i = 0; i < arr.length; i++) {
// arr[i] = "GG";
// }
// for (int i = 0; i < arr.length; i++) {
// System.out.println(arr[i]); //GG
// }
//方式二:增强for循环,相当于取出元素赋给s,改变的是S,原数组没变。
for (String s: arr) {
s = "GG";
}
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]); //MM
}
}
@Test // Cillection自带的forEach方法
public void test5(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
coll.forEach(System.out::println);
}
十二. List接口常用实现类的对比(基于Collection接口)
概述
由于数组存储数据的局限性,我们通常使用List替代数组。
List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
JDK API中List接口常用实现类有:ArrayList、LinkedList、和Vector。
我们常称list为动态数组。
java集合根接口有两个:Collection和 Map.
1. List接口:
存储有序的、可重复的数据 -->本质”动态“数组,替换原有的数组
1.1 ArrayList:
作为List接口的主要!实现类 jdk1.2出现 执行效率高, 线程不安全;底层使用Object[] elementData存储,仍存在数组中
1.2 LinkedList:
对于频繁的插入、删除操作,使用此类效率高于ArrayList;底层用双向链表存储,定义了node类型的first和last,记录首末元素,定义内部类Node,作为存储结构 jdk1.2出现
1.3 Vector:
作为List接口的古老实现类 jdk1.0出现 不怎么使用 执行效率低, 线程安全 源码中加同步sychnorized;底层使用Object[]elementData存储,仍存在数组中
面试题: ArrayList、LinkedList、Vector三者的异同???
同: 三个类都实现了List接口,存储数据的特点相同:存储有序、可重复的数据
不同点: 见上
2. ArraylList源码分析
ArrayList源码分析: 在 jdk 7 和jdk 8稍有不同:
------ jdk 7中: 初始化: ArrayList A1 = new ArrayList() 创建一个空的list底层长为10的object[]数组
添加数据:list.add(123) // elementData[0] = new Integer(123)
...
list.add(11); //如果此次添加导致底层elementData数组容量不够,则扩容,默认扩容为原来的1.5倍,将原数组数据copy到新数组中
结论:建议开发中使用带参构造器:ArrayList A2 = new ArrayList(int capacity)
------ jdk 8中变化: new初始化时,底层数组为{} 空, 没有分配长度,调用add时,再增加长度
ArrayList A1 = new ArrayList() // 底层数组初始化为{},并没有创建长度为10的数组;
list.add(123); //当我们第一次调用add()时,底层才创建了长度为10的数组,并将数据添加进去;
后续操作与jdk 7无异。
小结: 有什么好处?
节省内存,jdk7时其对象创建类似饿汉单例,jdk8类似懒汉单例,延迟了数组的创建,不急着分配内存。
3. LinkedList源码分析
LinkedList 源码分析:
LinkedList l1 = new LinkedList(); // 内部声明了Node类型的first和last属性,默认值为null
list.add(123); // 将123封装到了Node中,创建了Node对象
其中,Node定义为:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
此处也可看出其为双向链表prev,next
并无扩容的说法
Vector源码分析
关于Vector的源码分析,只简单说一下,扩容机制。
Vector源码分析: jdk7和8都创建了底层长度为10的数组
默认扩容为原来的2倍。(ArrayList为1.5倍)
问题: 假如多线程中遇到数据存储问题,还用ArrayList吗?
用,将list扔到synchronizedList中返回的就是线程安全的。
十三. List接口常用方法测试
List接口除了从Collection接口继承的15个方法以外,还添加了一些根据错因操作集合元素的方法。
-
Object get(int index): 获取指定index位置的元素
-
Object get(int index): 获取指定index位置的元素
-
Object get(int index): 获取指定index位置的元素
-
int indexOf(Object obj): 返回boj在集合中首次出现的位置
-
Object remove(int index): 移除指定index位置的元素,并返回此元素
-
Object set(int index, Object ele): 设置指定index位置的元素为ele
-
List subList(int fromIndex, int toIndex): 返回从fromIndex到toIndex位置的子集合
public class ListTest {
@Test
public void test1(){
ArrayList list = new ArrayList();
list.add(123);
list.add(456);
list.add("AA");
list.add(new Person("Tom", 12));
list.add(789);
System.out.println(list);
// [123, 456, AA, Person{name='Tom', age=12}, 789]
// 1. void add(int index, Object ele): 再index位置插入ele元素
list.add("BB");
System.out.println(list);
// [123, 456, AA, Person{name='Tom', age=12}, 789, BB]
// 2. boolean addAll(int index, Collection eles): 从index位置开始将eles的所有元素添加进来
List list1 = Arrays.asList(1, 2, 3);
list.addAll(list1); // 将全部元素加进来
//list.add(list1); // 将list1整体作为一个元素添加进来
System.out.println(list.size()); // 9
// 3. Object get(int index): 获取指定index位置的元素
System.out.println(list.get(0)); // 123
}
@Test
public void test2(){
ArrayList list2 = new ArrayList<>();
list2.add(123);
list2.add(456);
list2.add("AA");
list2.add(new Person("Tom", 12));
list2.add(456);
// int indexOf(Object obj): 返回boj在集合中首次出现的位置
int index = list2.indexOf(456);
System.out.println(index); // 1 若有返回首次出现的位置,若没有返回 -1
// int lastIndexOf(Object obj): 返回boj在集合中最后出现的位置, 不存在返回-1
System.out.println(list2.lastIndexOf(456)); // 4
// Object remove(int index): 移除指定index位置的元素,并返回此元素
Object obj = list2.remove(0);
System.out.println(obj); // 123
System.out.println(list2); // [456, AA, Person{name='Tom', age=12}, 456]
// Object set(int index, Object ele): 设置指定index位置的元素为ele
list2.set(1, "CC");
System.out.println(list2); // [456, CC, Person{name='Tom', age=12}, 456]
// List subList(int fromIndex, int toIndex): 返回从fromIndex到toIndex位置的子集合 左闭右开
List subList = list2.subList(2, 4);
System.out.println(subList); // [Person{name='Tom', age=12}, 456]
System.out.println(list2); // 本身list不变 [456, CC, Person{name='Tom', age=12}, 456]
}
}
List遍历及方法总结
总结常用方法:
总结:常用方法
* 增: add(Object obj)
* 删: remove(int index) / remove(Object obj)
* 改: Object set(int index, Object ele)
* 查: Object get(int index)
* 插: void add(int index, Object ele)
* 长度:size()
* 遍历:① Iterator迭代器方式
* ② 增强for循环
* ③ 普通的循环
遍历:
@Test
// 遍历
public void test3() {
ArrayList list = new ArrayList();
list.add(123);
list.add(456);
list.add("AA");
// 方式一:Iterator迭代器方式
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next()); // 123 \n 456 \n AA
}
// 方式二:
for (Object obj : list) {
System.out.println(obj);
}
// 方式三 : 普通for循环
for (int i = 0; i < list.size(); i++){
System.out.println(list.get(i));
}
}
集合list的一个面试题:
public class ListExercise {
@Test
public void test() {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
updateList(list);
System.out.println(list); // [1, 2]
// 此处考remove, 集合包装类将元素作为一个个对象装进去,remove有两种,因为索引最方便,所以此处默认调用的是索引的remove
}
private void updateList(List list) {
list.remove(2); // moren index索引
//list.remove(new Integer(2)); //人为调用装箱
}
}
十四. Set接口实现类的对比
|----Set接口:存储无序的、不可重复的数据 -->类似高中讲的”集合“
|----HashSet、LinkedHashSet、TreeSet
概述
- set接口是Collection的子接口,set接口没有提供额外的方法
- Set集合不允许包含相同的元素,添加两个相同元素到同一个Set集合中,操作失败
- Set判断两个对象是否相同,是根据equals()方法。切记不是使用==运算符
HashSet
HashSet的底层实现是new 了一个hashMap
/**
* Set接口的框架结构:
* |----Collection接口:单列集合,存储一个一个的对象。存储int,short等使用包装类
* * * |----Set接口:存储无序的、不可重复的数据 -->类似高中讲的”集合“
* * * |----HashSet:作为Set接口的主要实现类;线程不安全;可以存储null值;底层也使用数组村的
* |----LinkedHashSet:实质是HashSet的子类;遍历其内部数据时,可以按照添加时的顺序遍历,对于频繁的遍历操作,LinkedHasSet效率高于HashSet
* |----TreeSet:可以按照添加对象的指定属性进行排序;底层是红黑树
*
* 1. Set接口无额外定义的新方法,使用的都是Collection定义的方法
*/
public class SetTest {
/*
* 一. Set:存储无序、不可重复的数据
* 以HashSet为例说明:
* 1. 无序性:不等于随机性。是指存放时并非按照数组索引的顺序添加。而是根据数据的哈希值决定的。
*
* 2. 不可重复性:保证添加的元素按照equals()判断时,不能返回true.即:相同的元素只能添加一个
*
* 二. 添加元素的过程:以HashSet为例:假如每添加一个元素都要和所有其他元素比较,那么效率会很低。
* 所以这里用hashcode哈希值判断(数据结构中的哈希表)放入数组。若位置1为空,则直接放入;位置1不为空,则两者位置相同,开始比较,哈希值不一定相同。
* 哈希值不同时,以链表的形式添加到数组中;若哈希值相同时,则两者相同,添加失败。
*
* 以链表的形式添加到数组中:即在jdk7中是把新的元素放在数组位置,原先的元素放在新元素下面;
* 在jdk8中是把新元素放在数组原先元素的下边。 简称七上八下。
* (我们向HashSet中添加元素a,首先调用元素a所在类的hashcode()方法,计算元素a的哈希值,此哈希值接着通过某种算法
* 计算出在HashSet底层数组中的存放位置(即为,索引位置),判断数组此位置上是否已有元素:
* 如果此位置上没有其他元素,则元素a添加成功;---->情况1
* 如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较a与b的哈希值:
* 如果hash值不相同,则元素a添加成功; ---->情况2
* 如果hash值不相同,则需要调用元素a所在类的equals()方法:
* equals()返回true, 元素a添加失败;
* equals()返回false,元素a添加成功) ---->情况3
*
* 对于添加成功的情况2和情况3而言:元素a 与已经存在指定索引位置上的数据以链表的形式存储。
* jdk7:元素a放到数组中,指向原来的元素b
* jdk8:原来的元素b仍在数组中,指向元素a
* 总结: 七上八下
*
* HashSet底层:数组+链表的结构。
* */
@Test
public void test1(){
Set set = new HashSet();
set.add(123);
set.add(456);
set.add("AA");
set.add("BB");
set.add(new Person("Tom", 12));
set.add(new Person("Tom", 12)); //未重写hashcode,默认调用Object类中的hashCode(),而它随机算一个hash值,两者看似一样,但能添加成功
set.add(789);
System.out.println(set);
}
}
关于hashCode()和equals()的重写
同一个类中重写了hashCode()方法后,元素的添加就会相对比Object类hashCode()方法哈希值相同的概率高。那么就会进一步调用equals()方法去比较
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
if (age != person.age) return false;
return Objects.equals(name, person.name);
//return name != null ? name.equals(person.name) : person.name == null;
}
问题:为什么idea工具里hashCode()的重写,有 31 这个数字?
答:① 选择系数时尽量选择最大的系数,因为如果计算出来的hash地址越大,所谓的”冲突“就越少,查找起来效率也会越高。 (减少冲突)
②31 只占5bits,相乘造成的数据溢出的概率较小。
③31可以由 i*31 == (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
④31是一个素数,做乘法结果只能被本身、1以及素数来整除。(减少冲突)
要求:向set中添加的数据,其所在的类一定要重写hashCode()和equals()方法
要求:重写的这俩方法尽可能保持一致性——>相等的对象必须具有相等的散列码(equals相等的,hash值尽可能相等)
直接idea自动生成重写方法即可。
LinkedHashSet的使用
// LinkedHashSet的使用:
// 它作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录了前一个数据和后一个数据;
// 优点是,对于频繁的遍历操作,LinkedHasSet效率高于HashSet
@Test
public void test2(){
Set set = new LinkedHashSet();
set.add(123);
set.add(456);
set.add("AA");
set.add("BB");
set.add(new Person("Tom", 12));
set.add(new Person("Tom", 12));
set.add(789);
System.out.println(set); // [123, 456, AA, BB, Person{name='Tom', age=12}, 789]
// 遍历结果按照添加的顺序。原因是链表,每个元素之间都有指针指向,记录顺序
}
TreeSet的自然排序
public class TreeSetTest {
/**
* 1. 向TreeSet中添加的数据,要求是相同类的对象。
* 2. 两种排序方式:自然排序(实现comparable接口) 和 定制排序
* 3. 自然排序中,比较两个对象是否相同的标准为:compareTo() 返回0, 不再是equals()
* 底层是树形结构,红黑树存储结构(二叉树),所以不能存相同的元素
*/
@Test
public void test1() {
TreeSet set = new TreeSet();
// 添加失败,不能添加不同类的对象
// set.add(123);
// set.add(456);
// set.add("AA");
// set.add(new Person("zhansan", 15));
//例1 添加成功,因为都属于Integer类对象
// set.add(123);
// set.add(456);
// set.add(-1);
// 例2 报错:必须说明怎么排序. 所以将User类重写compareTo()方法后,按照name排序默认从小到大,再次输出
set.add(new User("Zhansan", 15));
set.add(new User("Lisi", 30));
set.add(new User("Tom", 15));
set.add(new User("Mike", 60));
// 例3 再次添加name相同,年龄不同的人,看能否成功
// 结果:没添加进去。因为compareTo判断两者name相同,返回0 所以不会添加进去。(目前是自然排序)
// 想添加进去,则重写compareTo()方法进行二级判断排序
set.add(new User("Mike", 13));
System.out.println(set); //[-1, 123, 456]
Iterator iter = set.iterator();
while (iter.hasNext()) {
System.out.println(iter.next()); // -1 123 456
}
// 例2输出如下
// User{name='Lisi', age=30}
//User{name='Mike', age=60}
//User{name='Tom', age=15}
//User{name='Zhansan', age=15}
// 例3 重写后输出如下
//User{name='Lisi', age=30}
//User{name='Mike', age=13}
//User{name='Mike', age=60}
//User{name='Tom', age=15}
//User{name='Zhansan', age=15}
}
}
User.java
public class User implements Comparable{
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
// 按照姓名从小大排列 ,从大到小只需return -结果
// 姓名从小到大排列,其次年龄从小到大排列
@Override
public int compareTo(Object o) {
if (o instanceof User) {
User user = (User) o;
//return this.name.compareTo(user.name);
int compare = this.name.compareTo(user.name);
if (compare != 0) {
return compare;
}else{
return Integer.compare(this.age,user.age);
}
}else{
throw new RuntimeException("输入的类型不匹配");
}
}
}
TreeSet的定制排序
// 定制排序
@Test
public void test2(){
Comparator com = new Comparator() {
// 按照年龄从小到大排序
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof User && o2 instanceof User) {
User u1 = (User)o1;
User u2 = (User)o2;
return Integer.compare(u1.getAge(), u2.getAge());
}else{
throw new RuntimeException("输入的数据类型不匹配");
}
}
};
TreeSet set = new TreeSet(com); // 不加参数默认按自然排序排序,加参数后按照参数的标准来排序
set.add(new User("Zhansan", 15));
set.add(new User("Lisi", 30));
set.add(new User("Tom", 15));
set.add(new User("Mike", 60));
set.add(new User("Merry", 60));
Iterator iter = set.iterator();
while (iter.hasNext()) {
System.out.println(iter.next()); // -1 123 456
}
//User{name='Zhansan', age=15}
//User{name='Lisi', age=30}
//User{name='Mike', age=60}
// 年龄相同时,按添加的先后顺序,先添加的存在,后添加的(Merry)添加失败
//User{name='Zhansan', age=15}
//User{name='Lisi', age=30}
//User{name='Mike', age=60}
}
十五. 集合-复习
- 集合collection中存储的如果是自定义类的对象,需要自定义类重写哪个方法?为什么?
equals()方法。 contains()/remove()/retainsAll()这些都要考虑equals()方法
List:重写equals()方法,add方法未调用equals,但是其余如remove等用到了equals方法
Set:(HashSet、LinkedHashSet为例):重写equals()、hashCode()
(TreeSet为例):Comparable:compareTo(Object o1)
Comparator:compare(Object o1, Object o2)
- ArrayList,LinkedList,Vector三者的相同点和不同点?
相同点:都实现了List接口,特点:有序、可重复
不同点:开发中常用前两个,vector作为古老实现类,不怎么用。vector线程安全,底层是数组,扩容2倍;ArrayList底层是数组,查找效率高,扩容1.5倍;LinkedList底层是双向链表,插入删除效率高。(不同点先比较ArrayList和Vector,再比较ArrayList和LinkedList)
List 、 Map 、Set
只要涉及到多个数据一般用list去装,set主要体现在不重复。
①比如一堆单词去和存放关键字的集合比较那些事关键字,用set比较合适。因为set不重复,效率较list高。
②如机场的禁飞名单,使用set存储。来一个人比较直接hash值判断,而不用一个个去比对。
- List接口的常用方法有哪些?
增:add(Object obj)
删:remove(Object o) / remove(int index)
改:set(int index, Object obj)
查:get(int index)
插:add(int index, Object obj)
长度:size()
遍历:Iterator;增强for循环;普通的for
-
举例说明Iterator和增强for循环遍历List。
-
set存储数据的特点?常见的实现类是什么?并说明其特点。
无序、不可重复。重点理解。
HashSet:(底层其实是HashMap)
LinkedHashSet:
TreeSet:
collection及colection遍历
一. 15个方法
add(Object obj),addAll(Collection coll),size(),isEmpty(), clear()
contains(Object obj),containsAll(Collection coll),remove(Object obj),removeAll(Collection coll),retainsAll(Collection coll),equalsAll(Object obj)
hashCode(),toArray(),iterator();toArray重载方法(泛型)
二. Collection集合与数组的转换
//8. 集合 --> 数组: toArray()
Object[] arr = coll.toArray();
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
// 拓展: 数组 --> 集合 asList
List<String> list = Arrays.asList(new String[]{"aa","bb"});
System.out.println(list); // [aa, bb]
//注意这里
List arr0 = Arrays.asList(new int[]{123, 456});
System.out.println(arr0); // [[I@3aeaafa6]输出了地址值,将整个int数组当成了一个整体
// 改正①
List arr1 = Arrays.asList(123, 456);
System.out.println(arr1); // [123, 456]
//改正②
List arr2 = Arrays.asList(new Integer[]{123, 456});
System.out.println(arr2); // [123, 456]
掌握要求:
层次一:选择合适的集合类去实现数据的保存,调用其内部的相关方法
层次二:不同的集合类底层的数据结构为何?如何实现数据的操作的,如增删改查?
三. 遍历Collection的两种方式
① 使用迭代器Iterator java.utils包下
Iterator iterator = coll.iterator();
while (iterator.hasNext())
{
// ①指针下移,因为开始时指针在第一个元素上方②将下移后的集合位置上的元素返回
System.out.println(iterator.next());
}
② foreach循环(或增强for循环)因为collection实现了Iterable接口
例: 直接调用forEach方法:(java8新特性)
@Test
public void test5(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
coll.forEach(System.out::println); //方法引用, lambda表达式
}
③ 迭代器中的remove方法
@Test
public void test3() {
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
Iterator iterator = coll.iterator();
while (iterator.hasNext())
{
Object obj = iterator.next();
if ("Tom".equals(obj))
{
iterator.remove();
}
}
//上面遍历完iterator已经走到末尾没有了,想要重新遍历需要再次获取一个iterator对象
iterator = coll.iterator();
while (iterator.hasNext())
{
System.out.println(iterator.next());
}
}
List接口
一. 存储数据的特点
|----Collection接口:单列集合,存储一个一个的对象。存储int,short等使用包装类
* * |----List接口:存储有序的、可重复的数据 -->本质”动态“数组,替换原有的数组
* * |----ArrayList : 作为List接口的主要!实现类 jdk1.2出现 执行效率高, 线程不安全;底层使用Object[] elementData存储,仍存在数组中
* |----LinkedList : 对于频繁的插入、删除操作,使用此类效率高于ArrayList;底层用双向链表存储,定义了node类型的first和last,记录首末元素,定义内部类Node,作为存储结构 jdk1.2出现
* /----Vector:作为List接口的古老实现类 jdk1.0出现 不怎么使用 执行效率低, 线程安全 源码中加同步sychnorized;底层使用Object[]elementData存储,仍存在数组中
二. 常用方法
* 增: add(Object obj)
* 删: remove(int index) / remove(Object obj)
* 改: Object set(int index, Object ele)
* 查: Object get(int index)
* 插: void add(int index, Object ele)
* 长度:size()
* 遍历:① Iterator迭代器方式
* ② 增强for循环
* ③ 普通的循环
三. 常用实现类
|----List接口:存储有序的、可重复的数据 -->本质”动态“数组,替换原有的数组
* * |----ArrayList : 作为List接口的主要!实现类 jdk1.2出现 执行效率高, 线程不安全;底层使用Object[] elementData存储,仍存在数组中
* |----LinkedList : 对于频繁的插入、删除操作,使用此类效率高于ArrayList;底层用双向链表存储,定义了node类型的first和last,记录首末元素,定义内部类Node,作为存储结构 jdk1.2出现
* /----Vector:作为List接口的古老实现类 jdk1.0出现 不怎么使用 执行效率低, 线程安全 源码中加同步sychnorized;底层使用Object[]elementData存储,仍存在数组中
四. 源码分析
* ArrayList源码分析: 在 jdk 7 和jdk 8稍有不同:
* ------ jdk 7中: 初始化: ArrayList A1 = new ArrayList() 创建一个空的list底层长为10的object[]数组
* 添加数据:list.add(123) // elementData[0] = new Integer(123)
* ...
* list.add(11); //如果此次添加导致底层elementData数组容量不够,则扩容,默认扩容为原来的1.5倍,将原数组数据copy到新数组中
* 结论:建议开发中使用带参构造器:ArrayList A2 = new ArrayList(int capacity)
*
* ------ jdk 8中变化: new初始化时,底层数组为{} 空, 没有分配长度,调用add时,再增加长度
* ArrayList A1 = new ArrayList() // 底层数组初始化为{},并没有创建长度为10的数组;
* list.add(123); //当我们第一次调用add()时,底层才创建了长度为10的数组,并将数据添加进去;
* 后续操作与jdk 7无异。
*
* 小结: 有什么好处?
* 节省内存,jdk7时其对象创建类似饿汉单例,jdk8类似懒汉单例,延迟了数组的创建,不急着分配内存。
*
* LinkedList 源码分析:
* LinkedList l1 = new LinkedList(); // 内部声明了Node类型的first和last属性,默认值为null
* list.add(123); // 将123封装到了Node中,创建了Node对象
* 其中,Node定义为:
* private static class Node<E> {
* E item;
* Node<E> next;
* Node<E> prev;
*
* Node(Node<E> prev, E element, Node<E> next) {
* this.item = element;
* this.next = next;
* this.prev = prev;
* }
* }
* 此处也可看出其为双向链表prev,next
* 并无扩容的说法
*
* Vector源码分析: jdk7和8都创建了底层长度为10的数组,扩容方面默认扩容为原来的2倍
- Vector源码分析: jdk7和8都创建了底层长度为10的数组,扩容方面默认扩容为原来的2倍; 栈是vector下的一个子类
五. 存储的元素要求
添加的对象,所在的类要重写equals方法
Set 接口
一. 存储数据特点
无序的、不可重复的。 注意理解
- 以HashSet为例说明:
-
- 无序性:不等于随机性。是指存放时并非按照数组索引的顺序添加。而是根据数据的哈希值决定的。
*
- 无序性:不等于随机性。是指存放时并非按照数组索引的顺序添加。而是根据数据的哈希值决定的。
-
- 不可重复性:保证添加的元素按照equals()判断时,不能返回true.即:相同的元素只能添加一个
-
二. 元素添加过程
假如每添加一个元素都要和所有其他元素比较,那么效率会很低。
* 所以这里用hashcode哈希值判断(数据结构中的哈希表)放入数组。若位置1为空,则直接放入;位置1不为空,则两者位置相同,开始比较,哈希值不一定相同。
* 哈希值不同时,以链表的形式添加到数组中;若哈希值相同时,则两者相同,添加失败。
*
* 以链表的形式添加到数组中:即在jdk7中是把新的元素放在数组位置,原先的元素放在新元素下面;
* 在jdk8中是把新元素放在数组原先元素的下边。 简称七上八下。
* (我们向HashSet中添加元素a,首先调用元素a所在类的hashcode()方法,计算元素a的哈希值,此哈希值接着通过某种算法
* 计算出在HashSet底层数组中的存放位置(即为,索引位置),判断数组此位置上是否已有元素:
* 如果此位置上没有其他元素,则元素a添加成功;---->情况1
* 如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较a与b的哈希值:
* 如果hash值不相同,则元素a添加成功; ---->情况2
* 如果hash值不相同,则需要调用元素a所在类的equals()方法:
* equals()返回true, 元素a添加失败;
* equals()返回false,元素a添加成功) ---->情况3
*
* 对于添加成功的情况2和情况3而言:元素a 与已经存在指定索引位置上的数据以链表的形式存储。
* jdk7:元素a放到数组中,指向原来的元素b
* jdk8:原来的元素b仍在数组中,指向元素a
* 总结: 七上八下
*
* HashSet底层:数组+链表的结构。
三. 常用方法
Set接口无额外定义的新方法,使用的都是Collection定义的方法
四。 常用实现类
|----Collection接口:单列集合,存储一个一个的对象。存储int,short等使用包装类
* * * |----Set接口:存储无序的、不可重复的数据 -->类似高中讲的”集合“
* * * |----HashSet:作为Set接口的主要实现类;线程不安全;可以存储null值;底层也使用数组村的
* |----LinkedHashSet:实质是HashSet的子类;可以按照添加时的顺序遍历;在添加数据的同时,每个数据还维护了两个引用,记录了前一个数据和后一个数据;
* // 优点是,对于频繁的遍历操作,LinkedHasSet效率高于HashSet
* |----TreeSet:可以按照添加对象的指定属性进行排序;底层是红黑树
五. 存储对象时,所在类的要求
HashSet / LinkedHashSet:所在类一定要重写hashCode()和equals().
要求:重写的hashCode()和equals()尽可能保持一致性:相等的对象必须具有相等的散列码。
重写小技巧:对象中用作equals()方法比较的Field,都应该用来计算hashCode值
TreeSet:
-
- 向TreeSet中添加的数据,要求是相同类的对象。
-
- 两种排序方式:自然排序(实现comparable接口) 和 定制排序
-
- 自然排序中,比较两个对象是否相同的标准为:compareTo() 返回0, 不再是equals()
- 底层是树形结构,红黑树存储结构(二叉树),所以不能存相同的元素
-
- 向TreeSet中添加的数据,要求是相同类的对象。
常用排序方式:
自然排序:
set.add(new User("Zhansan", 15));
set.add(new User("Lisi", 30));
set.add(new User("Tom", 15));
set.add(new User("Mike", 60));
Iterator iter = set.iterator();
while (iter.hasNext()) {
System.out.println(iter.next()); // -1 123 456
}
定制排序:
@Test
public void test2(){
Comparator com = new Comparator() {
// 按照年龄从小到大排序
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof User && o2 instanceof User) {
User u1 = (User)o1;
User u2 = (User)o2;
return Integer.compare(u1.getAge(), u2.getAge());
}else{
throw new RuntimeException("输入的数据类型不匹配");
}
}
};
TreeSet set = new TreeSet(com); // 不加参数默认按自然排序排序,加参数后按照参数的标准来排序
set.add(new User("Zhansan", 15));
set.add(new User("Lisi", 30));
set.add(new User("Tom", 15));
set.add(new User("Mike", 60));
set.add(new User("Merry", 60));
Iterator iter = set.iterator();
while (iter.hasNext()) {
System.out.println(iter.next()); // -1 123 456
}
//User{name='Zhansan', age=15}
//User{name='Lisi', age=30}
//User{name='Mike', age=60}
// 年龄相同时,按添加的先后顺序,先添加的存在,后添加的(Merry)添加失败
//User{name='Zhansan', age=15}
//User{name='Lisi', age=30}
//User{name='Mike', age=60}
}
TreeSet的课后练习
尚硅谷视频 P544 、
Set课后两道面试题
尚硅谷视频 P545
① 在List内去除重复数字值,尽量简洁
分析:将List数组传入方法,方法中new一个set集合,将list元素添加到set中,返回ArrayList(set)赋给list2,遍历