声明:该专栏本人重新过一遍java知识点时候的笔记汇总,主要是每天的知识点+题解,算是让自己巩固复习,也希望能给初学的朋友们一点帮助,大佬们不喜勿喷(抱拳了老铁!)
往期回顾
Java学习day15:Object类、set集合
一、Object类
Object类是Java中所有类的基类,学习Object类实际上就是在学习他下面的方法。
Object类有构造方法Object(),所以是可以直接实例化的。
1.1三个方法
1.1.1 public String toString()
返回的是对象的字符串表示形式
该toString类方法Object返回一个由其中的对象是一个实例,该符号字符的类的名称的字符串`@` ”和对象的哈希码(内存地址)的无符号的十六进制表示。
念起来很绕口,这句话的意思是“当你在一个对象上调用
toString
方法时,它会返回一个字符串,返回的字符串通常以@
开始,后面跟着对象的类名。除了类名之外,还会包含对象的哈希码的无符号十六进制表示。
例如,如果有一个字符串对象,它的toString
方法可能会返回类似于"java.lang.String@15db9742"
的字符串。这里的"java.lang.String@15db9742"
表示这个字符串对象是属于java.lang.String
类,而后面的15db9742
是该对象的哈希码的无符号十六进制表示。”
换句话说,这个方法返回一个等于下列值的字符串:
getClass().getName() + '@' + Integer.toHexString(hashCode())
说白了就是返回类名+@+对象16进制的内存地址
示例:
package org.example;
import java.util.*;
class Person{
String name;
int age;
}
public class Main {
public static void main(String[] args) {
Object obj=new Object();
Person person=new Person();
System.out.println(person);
}
控制台输出:
org.example.Person@7ef20235
toString()这个方法是打印对象时自动调用的,正常情况下会给出类名+@+对象16进制的内存地址为了结果让人容易阅读,建议所有的子类都覆盖并重写此方法。
示例:
class Person {
String name;
int age;
//person类是Object类子类不? 是!!!
//
@Override
public String toString() {
System.out.println("123");
return
"name=\"" + name + '\"' +
", age=" + age
;
}
}
public class Demo1 {
public static void main(String[] args) {
Object obj = new Object();
Person person = new Person();
System.out.println(person);
}
}
结果是name=" " ,age= ;
不知道大家是否看得懂重写的toString方法?如果看不懂多看几遍 ,其实就是个转义字符。
1.1.2 boolean eaqual(Object obj)
指示一些其他对象是否等于此。
public boolean equals(Object obj) {
return (this == obj);
}
Object 类下面的方法比较是两个对象的地址。不看内容的
可能有人会问:为啥String类下面的equals方法比较的是内容呢?String类继承了Object的equals方法时重写了Object类下面的。为啥重写?当父类的需求,满足不了子类的需求的时候要重写父类的方法
因为实际开发中比较内容的时候很多,如果要比较两个对象的内容是否一样? 如果两个对象的内容一样返回一个true。反之返回false,看一下重写的方法是怎么写的。
import java.util.Objects;
class Student {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
//重写equlas,要求去比较内容,如果内容一样的额话,返回true
//stu1.equals(stu2)
//stu2赋值给了 o 向上转型 Object o =new Student();
@Override
public boolean equals(Object o) {
if (this == o) {//比较是地址
return true;
}
//如果地址不一样的话,再去比较内容,如果内容一样也返回true
if (o instanceof Student) {
//才去比较值 name age
Student stu = (Student)o;//向下转型
//stu1.equals(stu2) stu就是stu2 this 是stu1
return stu.age == this.age && stu.name.equals(this.name);
}
return false;
}
}
public class Demo2 {
public static void main(String[] args) {
Student stu1 = new Student("老邢", 89);
Student stu2 = new Student("老邢", 89);
//stu1是Object类子类,用的是object 类面的equals方法
//Object类下面的equals方法比较是 地址 this==obj
//System.out.println(stu1.equals(stu2));//false
//现在我的需求是当两个对象的内容一致的时候返回的额是true
//内容不一样的时候,返回是false
//就意味着Object类的equals方法已经满足不了Student类的需求了
//咋解决? 重写equals方法
System.out.println(stu1.equals(stu2));//true
}
}
代码注释很详细,大家多看多写,一定要自己写。
这是自己重写的equals方法,可以看到逻辑上是先比较地址,地址一样的情况下直接就返回true,地址不一样再比较内容,用了一个instanceof,先保证比较的两个对象是一个类的有着相同的成员属性可以比较,再向下转型比较内容。
而在实际开发中是不会主动去重写的,因为可以直接idea生成,看看idea自动生成的。快捷键alt+insert
import java.util.Objects;
class Student1 {
String name;
int age;
public Student1(String name, int age) {
this.name = name;
this.age = age;
}
//重写equlas,要求去比较内容,如果内容一样的额话,返回true
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || this.getClass() != o.getClass()){
return false;
}
Student1 student1 = (Student1) o;
return age == student1.age && Objects.equals(name, student1.name);
}
}
public class Demo3 {
public static void main(String[] args) {
Student1 stu1 = new Student1("老邢", 89);
Student1 stu2 = new Student1("老邢", 89);
//stu1是Object类子类,用的是object 类面的equals方法
//Object类下面的equals方法比较是 地址 this==obj
//System.out.println(stu1.equals(stu2));//false
//现在我的需求是当两个对象的内容一致的时候返回的额是true
//内容不一样的时候,返回是false
//就意味着Object类的equals方法已经满足不了Student1类的需求了
//咋解决? 重写equals方法
System.out.println(stu1.equals(stu2));//true
}
}
大家可以对比一下,看看自己重写的和idea快捷键生成的区别所在。
1.1.3 int hashCode();
看这个方法之前先了解一个知识点:哈希码值
哈希码值:在Object类下面,将内存地址(十六进制的值)转为十进制的值,此时这个十进制的值就叫hash码。也就是hashcode方法的返回值,hash码。
对于Object类来说,对象不同,内存地址必然不可能相同,对应的hash值也就不可能相同
虽然但是,我们来看看下面这段代码
class Cat {}
public class Demo4 {
public static void main(String[] args) {
Cat cat1 = new Cat();
Cat cat2 = new Cat();
System.out.println(cat1.hashCode());
System.out.println(cat2.hashCode());
String str = new String("a");
String str1 = new String("b");
String str2 = new String("a");
System.out.println(str.hashCode());//97
System.out.println(str1.hashCode());//98
System.out.println(str2.hashCode());//97
}
}
//现在很尴尬的一个点,Object类的hash值是内存地址十进制的转换
//只要你内存地址不一样,hash值一定不一样
//但是你看看str和str2 ,内存地址不一样,但是
//hash值是一样的?咋回事?因为在String类中重写了hashCode方法
而基本上我们自定义类的时候,也都需要重写hashCode方法。
注意事项
1.只要在执行Java应用程序时多次在同一个对象上调用该方法, `hashCode`方法必须始终返回相同的整数,前提是修改了对象中`equals`比较中的信息。 该整数不需要从一个应用程序的执行到相同应用程序的另一个执行保持一致。 |
2.如果根据`equals(Object)`方法,两个对象相等,则在两个对象中的每个对象上调用`hashCode`方法必须产生相同的整数结果。 |
3.不要求如果两个对象根据[`equals(java.lang.Object)`]方法不相等,那么在两个对象中的每个对象上调用`hashCode`方法必须产生不同的整数结果。 但是,程序员应该意识到,为不等对象生成不同的整数结果可能会提高哈希表的性能。 |
最需要重视的,就是第二条。如果equals方法判断了两个对象相等,结果为true,那么hashcode方法必须产生相同的整数结果。
所以大家会发现,一般我们在idea中快捷生成equals方法的重写的时候,idea会同步重写hashcode方法,就是因为这一条规则的存在。
(再次提醒:注意,无论何时重写equals方法,通常需要重写`hashCode`方法,以便维护`hashCode`方法的通用合同,该方法规定相等的对象必须具有相等的哈希码)
同时我们也要意识到,我们是可以自己重写hashcode方法的,只要保证满足这个规则,就可以自己随意重写。
示例:
import java.util.Objects;
class Dog {
int id;
String name;
public Dog(int id, String name) {
this.id = id;
this.name = name;
}
public boolean equals (Object o) {
if (this == o) {
return true;
}
if (o instanceof Dog) {
Dog dog = (Dog)o;
return this.id == dog.id && dog.name.equals(this.name);
}
return false;
}
@Override
public int hashCode() {
return name.hashCode() + id;
}
}
public class Demo5 {
public static void main(String[] args) {
Dog dog1 = new Dog( 3, "a");
Dog dog2 = new Dog( 3, "a");
//现在关注的是内容,如果内容一样 调用equals方法的时候
//必须返回一个true
System.out.println(dog1.equals(dog2));//true
System.out.println(dog1.hashCode());
System.out.println(dog2.hashCode());
}
}
没有重写hashCode时这个两个对象的hash值一样不一样?是不一样的,因为内存地址不一样的 如果根据equals(Object)方法两个对象相等,则在两个对象中的每个对象上调用hashCode方法必须产生相同的整数结果。所以我们重写hashCode方法。
上面这个两个对象的值是一样的,下面我们改进一下
import java.util.Objects;
class Dog {
int id;
String name;
public Dog(int id, String name) {
this.id = id;
this.name = name;
}
public boolean equals (Object o) {
if (this == o) {
return true;
}
if (o instanceof Dog) {
Dog dog = (Dog)o;
return this.id == dog.id && dog.name.equals(this.name);
}
return false;
}
@Override
public int hashCode() {
return name.hashCode() + id;
}
}
//同时改进一下,把两个对象的参数改成不一样的
public class Demo5 {
public static void main(String[] args) {
Dog dog1 = new Dog( 3, "a");
Dog dog2 = new Dog( 2, "b");
//现在关注的是内容,如果内容一样 调用equals方法的时候
//必须返回一个true
System.out.println(dog1.equals(dog2));//false
System.out.println(dog1.hashCode());
System.out.println(dog2.hashCode());
}
}
这个时候你会发现,两个对象的内容是不相等的了,内存地址就更不相等了,但是,但是,你会发现,hashcode值是相等的,一个是3+97,一个是2+98,结果都是100
所以得出结论,如果两个对象的hashcode值一样,对象是不一定一样的,但是如果两个对象相等,那么hash值一定相等。
hash值不一样,对象不一定一样。
对象一样的话,hash值一定一样
最后我们再看一个String类型的
import java.util.Objects;
class Panda {
int id;
String name;
public Panda(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Panda panda = (Panda) o;
return id == panda.id && Objects.equals(name, panda.name);
}
@Override
public int hashCode() {
return id;
}
}
public class Demo6 {
public static void main(String[] args) {
String str1 = new String("ab");
String str2 = new String("a");
System.out.println(str1.equals(str2));//true
//31 * 97+ 98
System.out.println(str1.hashCode());//3105
System.out.println(str2.hashCode());
//STring类下面的hashCode重写的Object类下面的
Integer i1 = new Integer(45);
Integer i2 = new Integer(45);
System.out.println(i1 == i2);//false
System.out.println(i1.equals(i2));//true
//大胆猜测一下 hash值是啥?
System.out.println(i1.hashCode());
System.out.println(i2.hashCode());
Panda panda1 = new Panda(67, "狗蛋");
Panda panda2 = new Panda(67, "狗蛋");
System.out.println(panda2.equals(panda1));
System.out.println(panda1.hashCode());
System.out.println(panda2.hashCode());
}
}
这里的31*97+98,是string重写的hashcode方法里封装的算法,属于是底层实现了,可以不用管
以上就是hashCode()方法的总结,说实话,我写这段总结的时候自己都是有点懵的,慢慢地在写的过程中才回忆起知识点。写的不完善也有点乱,大家更多地建议看相关视频讲解,还有就是,多敲代码!
二、set集合
2.1set集合特点
Set集合也是用来存储数据的,是collection接口的子接口。
存储数据的特征: 无序的 不可重复的,注意这个不可重复是指hash值不一样(这一点是区别于List接口的,List接口是有序的,可重复的)
2.2两个实现类HashSet和TreeSet
2.2.1 HashSet
依靠hash值进行存储的,如果两个元素hash值一样的话,就不再存储了。
HashSet这个类的方法和Collection接口和Set接口下面一样的,也有自己独有的方法,和ArrayList特别像,我们先通过一段代码来感受。
import java.util.HashSet;
import java.util.Set;
public class Demo1 {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
//set集合存储的无序的 不可重复的
set.add("b");
set.add("a");
set.add("d");
set.add("c");
set.add("a");
set.add("ad");
System.out.println(set);
Set<Integer> set1 = new HashSet<>();
set1.add(78);
set1.add(23);
set1.add(100);
set1.add(56);
System.out.println(set1);
set1.remove(23);
//循环
for (Integer integer : set1) {
System.out.println(integer);
}
}
}
通过这段代码的输出结果能够很明确的感受到无序不可重复性这个特点,同时注意,正是由于其无序性,没法直接用for循环进行遍历,因为没有下标 。
解决办法就是用增强for循环
for (Integer integer : set1) {
System.out.println(integer);
}
2.2.1.1HashSet集合存对象
存对象的知识点和collection集合和List接口一样的,就是先创建一个类,后面set里存放相应的对象,但是要注意,创建的类为了便于理解,要重写tostring方法。
同时要知道,在调用add方法添加对象的时候,底层在调用hashCode方法和equals,如果说两个对象的hash值一样,根据我们之前说的set集合的特性,无序不可重复,那么只会存放一个对象,所以就会看到有时候两个对象的内容值一样但是hash值不一样,所以两个都会存进去,就好像重复了一样。
而真实开发的时候,只关注内容的,如果内容一样,我也让你存不进去。!!!这个时候就需要重写equals方法和hahsCode方法。
在添加的时候,注意是先调用hashCode方法,再调用equals方法,也就是说,会先判断hash值是否一样,如果一样,再比较内容,内容也一样就不存,如果hash值不一样,那就直接存。
hash值不一样,对象不一定一样。
对象一样的话,hash值一定一样
再次重申知识点!
接下来看示例:
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
class Person {
int id;
String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return id == person.id && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
public class Demo2 {
public static void main(String[] args) {
Person person1 = new Person(1, "zhangsan");
Person person2 = new Person(1, "李四");
Person person3 = new Person(1, "李四");
Person person4 = new Person(1, "李四");
Set<Person> set = new HashSet<>();
//在调用add方法的时候 底层在调用hashCode方法和equals
set.add(person1);
set.add(person2);
set.add(person3);
set.add(person4);
System.out.println(set);
//感觉不太合适,发现存的两个对象的内容是一样。真实开发的时候
//只关注内容的,如果内容一样,我也让你存不进去。!!!
//总结:以后set集合中如果想存对象的时候,要求对象的内容如果一样的
//话,不能存到set集合中,咋办?重写equals方法和hahsCode方法
//hash值不一样,对象不一定一样。
//对象一样的话,hash值一定一样
}
}
2.2.2TreeSet
也是实现了Set集合,可以保证数据 唯一性,存储也是无序的。底层是二叉树里的红黑树,对存储数据进行自然排序 。
2.2.2.1底层结构
说结构之前,我们要明确什么是数据结构,数据结构,其实就是存储数据的各种结构,分为线性的和非线性的,线性的比如数组,非线性的比如树、图。说起来不难,但是想要搞懂各类数据结构及其底层原理实现,还是要点水平的。
我们先简单了解一下底层结构——二叉树。
通过查阅API我们得知TreeSet集合是基于TreeMap的实现,而TreeMap是基于二叉树(红黑树)结构,也就是说TreeSet集合的底层使用的二叉树(红黑树)结构。
树结构:它也是数据结构中的一种。在计算机领域中树结构指的是倒立的树。
树结构存储的数据,每个数据也需要节点来保存。
而TreeSet集合底层是二叉树的数据结构,什么是二叉树呢?
二叉树:每个节点的下面最多只能有2个子节点。
说明:最多表示一个节点下面可以有两个子节点或者一个子节点或者没有子节点。
在二叉树的根节点左侧的节点称为左子树,在根节点的右侧的节点称为右子树。
既然已经得知TreeSet集合底层是二叉树,那么二叉树是怎样存储数据的呢?是怎样保证存储的数据唯一并有序的呢?
二叉树的存储流程:
当存储一个元素的时候,如果是树的第一个元素,这个元素就作为根节点。
如果不是第一个元素,那么就拿要存储的元素与根节点进行比较大小:
大于根元素:就将要存储的元素放到根节点的右侧,作为右叶子节点。
等于根元素:丢弃。
小于根元素:就将要存储的元素放到根节点的左侧,作为左叶子节点。
总结:二叉树是通过比较大小来保证元素唯一和排序的。
比如:用二叉树对一下数据排序
20 10 31 5 13 23 51
大家可以看看这个博主的,拓展学习。
2.2.2.2TreeSet存数据
TreeSet简单的存数据,基本和hashset一样
package com.qfedu.c_treeSet;
import java.util.Set;
import java.util.TreeSet;
public class Demo1 {
public static void main(String[] args) {
//TreeSet在存储的数据的时候 会排序
Set<Integer> set = new TreeSet<>();
set.add(89);
set.add(79);
set.add(69);
set.add(109);
set.add(39);
System.out.println(set);
Set<String> set1 = new TreeSet<>();
set1.add("d");
set1.add("w");
set1.add("a");
set1.add("c");
System.out.println(set1);
}
}
重点是TreeSet集合中存自定义对象
想要在TreeSet集合中添加对象,必须要去实现Comparable这个接口
抽象方法:
| int | compareTo(T o)将此对象与指定的对象进行比较以进行排序。 |
返回值是int类型,但是形参是泛型
将此对象与指定的对象进行比较以进行排序。 返回一个负整数,零或正整数,因为该对象小于,等于或大于指定对象。
所以说,如果想要在TreeSet集合中存自定义对象,具体的操作步骤
1.首先需要创建一个类,这个类去实现Comparable接口 |
2.其次需要在类里重写compareTo()方法,注意这里面就选取类的一个int属性进行比较就好。返回值为int,形参就直接写成这个类的类型,本来就是比较同一个类的不同对象,就没必要再写泛型 |
3.在主类里创建多个对象并调用compareTo()进行比较 |
4.最终treeset会对存入的对象按其compareTo()方法返回的int值从小到大排序,这个排序次数不定, |
如果不重写,那么在存数据的时候就会报类转换异常的错误。
// Exception in thread "main" java.lang.ClassCastException:
// com.qfedu.c_treeSet.Student cannot be cast to java.lang.Comparable
// at java.util.TreeMap.compare(TreeMap.java:1294)
//因为Student转换不了Comparable
//而底层在进行排序的时候,实现了Comparable这个接口
示例
import java.util.Set;
import java.util.TreeSet;
class Student implements Comparable<Student>{
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Student o) {
System.out.println("123");
int num = this.age - o.age;
return num;
}
}
public class Demo2 {
public static void main(String[] args) {
Student stu1 = new Student("老邢", 45);
Student stu2 = new Student("老邢", 35);
Student stu3 = new Student("saolei", 25);
Student stu4 = new Student("老万", 87);
//按照年龄进行排序 存到TreeSet集合中
Set<Student> set = new TreeSet<>();
set.add(stu1);
set.add(stu2);
set.add(stu3);
set.add(stu4);
System.out.println(set);
}
}
我们分析代码,利用断点逐个跟进看代码运行情况
> 得有一个int类数据
>
> 好好思考一个问题:你得给我返回一个int类型的数据
>
> stu1有age变量 stu1的年龄 减去 stu2的年领
>
> 如果年龄返回值是一个负数的话: stu1的年领小与 stu 2
>
> 如果年龄返回值是一个0的话,stu1的年龄和stu2年龄相等
>
> 如果年龄返回值是一个正整数的话: stu1的年领大于 stu 2stu1 45
stu2 35
stu3 25
compareTo
set.add(stu1); 第一次调用compareTo
stu1和stu1在比较 45-45 =0 只保留 stu1set.add(stu2)的时候
又调用compareTo() 第二次调用compareTo
o:stu1
this: stu2 35 - 45 = -10 负数 stu2 比stu1小 咋排 [stu2, stu1]set.add(stu3)的时候 第三次调用compareTo
this: stu3
o: stu1
[stu3, stu1]第四次调用compareTo
this:stu3
o:stu2 [stu3 stu2]
[stu3 stu2 stu1]
基本上是非常详细的解释了底层原理,大家多看多想多写。
另外注意:如果是String类型或者Integer之类的,java是已经封装好了的,直接调用compareTo()方法就是,String类型是比较每个字符串的Unicode码。
我们再看几个示例:
1.使用TreeSet存储Employee对象,比较两个属性
int age, int weight 先按照年龄进行升序排,如果年龄相等的话,按照体重升序排
import java.util.Set;
import java.util.TreeSet;
class Employee implements Comparable<Employee>{
String name;
int age;
int weight;
public Employee(String name, int age, int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
", weight=" + weight +
'}';
}
@Override
public int compareTo(Employee o) {
//先按照年两比,如果年龄相等 就比较体重
int num = this.age - o.age;
if (num == 0) {
int num1 = o.weight - this.weight;
return num1;
}
return num;
}
}
public class Demo2 {
public static void main(String[] args) {
Set<Employee> set = new TreeSet<>();
set.add(new Employee("广坤", 35, 78));
set.add(new Employee("二贝", 26, 70));
set.add(new Employee("赵四", 35, 72));
set.add(new Employee("彩云", 35, 79));
set.add(new Employee("鸡哥", 32, 59));
set.add(new Employee("正经博", 32, 59));
System.out.println(set);
}
}
这里面需要着重注意的就是重写的compareto方法,还有就是this和o分别指代,顺序不同决定了最后是从小到大排还是从大到小排
2.TreeSet里面存的是Dog类,
两个属性: String name, int age
先按照字符串的字典顺序排,然后字符串相等的话,在按照年龄排
import java.util.Set;
import java.util.TreeSet;
class Dog implements Comparable<Dog>{
String name;
int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Dog o) {
//先按照字典的顺序进行排,如果字符串相等再按照年龄升序排
int num = this.name.compareTo(o.name);
if (num == 0) {
//字符串相等的情况,又要比较年领
int num1 = this.age - o.age;
return num1;
}
return num;
}
}
public class Demo3 {
public static void main(String[] args) {
Set<Dog> set = new TreeSet<>();
set.add(new Dog("彩云", 5));
set.add(new Dog("旺财", 2));
set.add(new Dog("大黄", 6));
set.add(new Dog("大黄", 3));
set.add(new Dog("大黄", 4));
System.out.println(set);
}
}
这个的变化就是,比较的是string类型,在自己重写的compareto方法里又重新调用了string的compareto方法,但不是递归,要想清楚。
以上,就是今天的所有知识点了。hashCode有点小混乱,需要多理解,set集合应该是比奥清楚的,就是要记的背的东西很多,大家得多花点时间,静下心慢慢看,慢慢理解,不是什么大问题。数据结构的知识点,大家可以下来自行再拓展。
加油吧,预祝大家变得更强!