比较器

当我们需要实现对象的排序问题的时候,就要使用到Java的比较器。

Java实现对象排序的接口有两个:

  • 自然排序:java.lang.Comparable
  • 定制排序:java.util.Comparator

自然排序

String、包装类等都默认实现了Comparable接口,重写了comparaTo(obj)方法,因此都可以直接使用自然排序。

自定义类若想实现自然排序,我们需要去实现Comparable接口,然后重写comparaTo(obj)方法方法,重写comparaTo(obj)方法具有一定的规则:

如果当前对象this大于形参对象obj,则返回正整数;如果当前对象this小于形参对象obj,则返回负整数;如果相等,则返回0。

1
2
3
4
5
6
7
8
9
10
11
12
13
public int compareTo(Object o) {
if(o instanceof Goods){//判断是否为Goods类型
Goods goods = (Goods) o;//将Object类型转换为Goods类型
if(this.price > goods.price){
return 1;
} else if(this.price < goods.price){
return -1;
} else{
return -this.name.compareTo(goods.name);//若全部相等,则按照name进行比较
}
}
throw new RuntimeException("传入数据有误");//不是Goods类型
}

定制排序

当元素的类没有实现Comparable接口而又不便修改代码,或是实现了Comparable接口的排序不适合当前操作,则我们可以使用Comparator的对象进行排序。

由于Comparator也是一个接口,同样需要重写方法,我们需要重写compare(Object o1, Object o2)方法,重写规则与自然排序相类似:

如果返回正整数,则代表o1大于o2;如果返回负整数,则代表o1小于o2;如果返回0,则表示o1与o2相等。

使用的方式是在调用sort()等排序方法的时候,在参数列表添加Comparator的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Arrays.sort(goods, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof Goods && o2 instanceof Goods){
Goods goods1 = (Goods) o1;
Goods goods2 = (Goods) o2;
if(goods1.getName().equals(goods2.getName())){
return Double.compare(goods1.getPrice(), goods2.getPrice());
}else {
return goods1.getName().compareTo(goods2.getName());
}
}
throw new RuntimeException("传入数据有误");
}
});

集合

Java集合是Java的一种容器,可以动态的把多个对象的引用放入容器中。集合与数组类似,都是Java容器,但是数组作为存储对象的容器方面具有一些弊端。

数组在内存存储方式的特点:

  1. 数组初始化以后,长度是确定的
  2. 数组声明的类型,决定了元素初始化的类型

数组在存储数据方面的弊端:

  1. 数组初始化以后,长度不可改变,不可拓展
  2. 数组提供的属性和方法太少,不便于增删改操作,且往往效率低,而且无法直接获取存储元素的个数(只能通过遍历等方法)
  3. 数组存储的数据是有序的,可以重复的,导致存储数组的特点单一

Java集合可以分为两种体系:

  • Collection接口:单列数组,定义了一组对象的方法的集合

    • List接口:元素有序、可重复的集合
    • Set接口:元素无序、不可重复的集合

    image-20220616113327310

  • Map接口:双列数据,保存具有映射关系“Key-value对”的集合,类似于“函数”,一个key只能对应一个value,一个value可以有多个key

    image-20220616113350677

Collection接口

Collection接口常用的方法

Collection常用方法

List接口:有序、可重复

迭代方式:fori配合size() 与 for-each

1
2
3
//size()方法用于计算List的大小
for (int i = 0; i < list.size(); i++){}
for (Object i : list){}
  • LinkedList:链表,查询慢、增删快,无同步,线程不安全
  • ArrayList:动态数组,查询快、删减慢,无同步,线程不安全
  • Vector:动态数组,查询快、删减慢,同步,线程安全,但效率相较低

这里写图片描述

Set接口:无序、不可重复

无序性不等于随机性,无序性指的是存放的元素不是按照索引的顺序添加的,而是根据元素的哈希值决定的。

迭代方式:for-each,不可以用fori迭代。

  • HashSet:基于HashMap实现,Set接口的主要实现类,它不会记录插入的顺序。HashSet不是线程安全的。

    添加元素的过程:

    1. 对添加的元素计算哈希值。当添加元素为对象时,一般而言,我们都会在对象的类中重写hashCode()方法,若不重写,则Object继承下来的hashCode()的作用仅为生成一个随机数

      image-20220622215950302

    2. 取16(定义的底层数组的默认容量)的模,放入对应的位置

    3. 若遇到取模相同的,哈希值不同时,则会使用链表的方式存储,jdk7时,会使新元素替代原元素,并指向原元素,jdk8则会让原元素指向新元素

    4. 若遇到哈希值相同时,则需要equals()比较,若equals()相同则不填入,若equals()不相同,则会使用链表

  • LinkedHashSet:是HashSet的子类,使用链表使得迭代结果是添加顺序的。

  • TreeSet:底层为红黑树结构存储,可以实现排序的实现类,要求放入的元素为相同类的对象。向TreeSet中添加元素,在遍历时默认会以自然排序的顺序遍历。

    若我们添加的元素是对象之类的,由于没有比较的方式,则会报错,我们需要重写对象的类的compareTo()方法。自然排序实际上是实现了Comparable接口。在自然排序中,判断对象是否相等的标准为compareTo()返回是否为0,不再是equals。

    而定制排序则是实现了Comparator接口,我们要使用定制排序,则需要在new TreeSet的时候,使用他的有参构造器,在参数内填入Comparator接口实现类的对象。在定制排序中,判断对象是否相等的标准为compare()返回是否为0,不再是equals。

Set接口里没有定义新的方法。

向Set接口的实现类添加的元素,一定需要重写hashCode()和equals()方法。且两个方法一定要保持规则的一致性,保证具有相等的散列码。

使用Iterator接口遍历

Iterator对象成为迭代器(设计模式的一种),主要用于遍历Collection集合中的元素。

迭代器定义:提供一种方法访问一个容器对象中各个元素,而不需要暴露该对象的内部细节。

Collection接口继承了java.lang.Iterable接口,因此它的实现类都提供了一个iterator()方法,它可以返回一个Iterable接口的对象,集合对象每次调用iterator()都会得到一个全新的迭代器对象,默认游标为第一个元素之前

得到一个Iterable接口对象后,我们可以用Iterable的方法来获取集合内的元素:

1
2
3
while (iterator.hasNext()){//hasNext()判断有无下一个元素
System.out.println(iterator.next());//next()获取下一个元素
}

Iterable有一个方法remove(),可以移除集合中的当前元素。

我们也可以使用for-each循环遍历集合,这样可以不使用Iterable接口。

Map接口

Map接口的常用方法

这里写图片描述

Map接口的实现类

  • HashMap:作为Map的主要实现类。线程不安全,效率高。可以存储null的key和value。

  • LinkedHashMap:HashMap的子类。添加了一个链表的机构,可以使得在遍历时,按照添加顺序遍历。对于频繁的遍历操作,可以使用这个实现类。

  • TreeMap:可以按照添加的Key进行排序,按照自然排序或定制排序遍历,类似于TreeSet,底层实现为红黑树。

    Map的特殊方法:

    这里写图片描述

  • Hashtable:最早的实现类,在Map出现之前就有。线程安全,效率低。不可以存储null的key和value。

  • Properties:Hashtable的子类。常用于处理配置文件,key和value都是String类型。

总结

  • ArrayXxx:底层数据结构是数组,查询快,增删慢
  • LinkedXxx:底层数据结构是链表,查询慢,增删快
  • HashXxx:底层数据结构是哈希表。依赖两个方法:hashCode()和equals()
  • TreeXxx:底层数据结构是二叉树。两种方式排序:自然排序和比较器排序

Lambda表达式

Lambda是一个匿名函数,可以理解为一段可以传递的代码。使用Lambda表达式可以使得我们的代码更加简洁。

Java的Lambda表达式本质是作为接口的实例。

使用举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//未使用Lambda表达式
Comparator<Integer> com1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
};
int compare1 = com1.compare(32, 31);
System.out.println(compare1);
//使用Lambda表达式
Comparator<Integer> com2 = (o1, o2) -> Integer.compare(o1, o2);
int compare2 = com2.compare(32, 31);
System.out.println(compare2);
//使用方法引用
Comparator<Integer> com3 = Integer::compare;
int compare3 = com3.compare(32, 31);
System.out.println(compare3);

使用方法

首先我们看到举例代码内的主要部分:

1
(o1, o2) -> Integer.compare(o1, o2)

格式:
-> :Lambda操作符 / 箭头操作符
左侧:Lambda形参列表 实际上即为接口中抽象方法的形参
右侧:Lambda体 实际上即为实现的抽象方法的方法体

具体格式大致可分为六种情况:

  1. 无参无返回值

    不使用Lambda表达式:

    1
    2
    3
    4
    5
    6
    7
    Runnable runnable = new Runnable(){
    @Override
    public void run() {
    System.out.println("test");
    }
    };
    runnable.run();

    使用Lambda表达式:

    1
    2
    Runnable runnable2 = () -> System.out.println("test");
    runnable2.run();
  2. 有参无返回值

    不使用Lambda表达式:

    1
    2
    3
    4
    5
    6
    7
    Consumer<String> con1 = new Consumer<>(){
    @Override
    public void accept(String s) {
    System.out.println(s);
    }
    };
    con1.accept("114514");

    使用Lambda表达式:

    1
    2
    3
    4
    Consumer<String> con2 = (String s) -> {
    System.out.println(s)
    };
    con2.accept("1919");
  3. 数据类型省略 "类型推断"

    上面一种情况,编译器可以判断出数据类型,因此我们可以省略数据类型:

    1
    2
    3
    4
    Consumer<String> con3 = (s) -> {
    System.out.println(s)
    };
    con3.accept("810");
  4. 只需要一个参数时,可省略参数小括号

    上面的情况,因为只有一个参数,因此我们可以省略参数的小括号:

    1
    2
    3
    4
    Consumer<String> con2 = s -> {
    System.out.println(s)
    };
    con2.accept("没活了");
  5. 多参数且多条执行语句,且拥有返回值

    不使用Lambda表达式:

    1
    2
    3
    4
    5
    6
    7
    8
    Comparator<Integer> com1 = new Comparator<>(){
    @Override
    public int compare(Integer o1, Integer o2) {
    System.out.println(o1);
    System.out.println(o2);
    return o1.compareTo(o2);
    }
    };

    使用Lambda表达式:

    1
    2
    3
    4
    5
    Comparator<Integer> com2 = (o1, o2) -> {
    System.out.println(o1);
    System.out.println(o2);
    return o1.compareTo(o2);
    };
  6. 当Lambda体只有一条语句时,return与大括号可以省略

    不使用Lambda表达式:

    1
    2
    3
    4
    5
    6
    Comparator<Integer> com1 = new Comparator<>(){
    @Override
    public int compare(Integer o1, Integer o2) {
    return o1.compareTo(o2);
    }
    };

    使用Lambda表达式:

    1
    Comparator<Integer> com2 = (o1, o2) -> o1.compareTo(o2);

泛型

泛型是 JDK 5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

注:例子来源 Java 基础 - 泛型机制详解 | Java 全栈知识体系 (pdai.tech)

泛型的作用

  • 代码复用

    在没有泛型的情况下,如果我们想实现不同类型的加法,我们需要每种类型都重载一个add方法(如下):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private static int add(int a, int b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
    }

    private static float add(float a, float b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
    }

    private static double add(double a, double b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
    }

    通过泛型,我们可以复用为一个方法:

    1
    2
    3
    4
    private static <T extends Number> double add(T a, T b) {
    System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
    return a.doubleValue() + b.doubleValue();
    }

    实际上这就是一种模板开发的方式,在实例化前不指定T的类型,在实例化时再去指定,可以达到代码复用的作用。

  • 类型安全

    泛型中的类型在使用时指定,不需要强制类型转换

    1
    2
    3
    4
    List list = new ArrayList();
    list.add("String");
    list.add(100d);
    list.add(new Person());

    在使用list时,list里的元素是Object类型的,我们无法约束其中的类型,所以当我们取出元素的时候,很容易会出现类型转换错误的问题。

    使用泛型可以达到类型约束的效果,提供了一个编译前的检查:

    1
    List<String> list = new ArrayList<String>();

泛型的使用

泛型的使用大概分为三种类型:泛型类、泛型接口、泛型方法

泛型类、泛型接口

泛型类和泛型接口是一种模板化开发的思想,也就是我们提及的第一个作用。

如果定义了泛型类,在实例化时没有指定类的泛型,则认为此泛型为Object,不建议这样使用。

一个简单的泛型类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Point<T>{         // 此处可以随便写标识符号,T是type的简称  
private T var ; // var的类型由T指定,即:由外部指定
public T getVar(){ // 返回值的类型由外部决定
return var ;
}
public void setVar(T var){ // 设置的类型也由外部决定
this.var = var ;
}
}
public class GenericsDemo06{
public static void main(String args[]){
Point<String> p = new Point<String>() ; // 里面的var类型为String类型
p.setVar("it") ; // 设置字符串
System.out.println(p.getVar().length()) ; // 取得字符串的长度
}
}

多元泛型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Notepad<K,V>{       // 此处指定了两个泛型类型  
private K key ; // 此变量的类型由外部决定
private V value ; // 此变量的类型由外部决定
public K getKey(){
return this.key ;
}
public V getValue(){
return this.value ;
}
public void setKey(K key){
this.key = key ;
}
public void setValue(V value){
this.value = value ;
}
}
public class GenericsDemo09{
public static void main(String args[]){
Notepad<String,Integer> t = null ; // 定义两个泛型类型的对象
t = new Notepad<String,Integer>() ; // 里面的key为String,value为Integer
t.setKey("汤姆") ; // 设置第一个内容
t.setValue(20) ; // 设置第二个内容
System.out.print("姓名;" + t.getKey()) ; // 取得信息
System.out.print(",年龄;" + t.getValue()) ; // 取得信息

}
}

泛型方法

泛型方法的使用并不是简单将类型替换为泛型,以下例子均不是泛型方法:

1
2
3
4
5
6
public void setKey(K key){  
this.key = key ;
}
public void setValue(V value){
this.value = value ;
}