本篇主要涉及Java基础部分中一些较难以理解的部分,主要涉及到 内部类、包装类、String相关、异常等
内部类
(非)静态内部类
public class TestInnerClass {
public static void main(String[] args) {
Face f = new Face();
//非静态内部类,必须要先创建外部类,再创建内部类
Face.Nose n = f.new Nose();
n.breath();
//静态内部类,不必先创建外部类再创建内部类
Face.Ear e = new Face.Ear();
e.listen();
}
}
class Face{
// 外部类的属性
String type = "瓜子脸";
static String color = "Red";
/**
* 非静态内部类:必须要先创建外部类,再创建内部类,可以看成是一个对象的成员属性
*/
class Nose{
//内部类不能定义成static的方法和属性
String type;
void breath(){
System.out.println(Face.this.type); //内部类可以调用外部类的内容
System.out.println("呼吸");
}
}
/**
* 静态内部类:当一个静态内部类存在,并不一定存在对应的外部类对象
*/
static class Ear{
//静态内部类的实例方法不能直接访问外部类的实例方法,因为外部类不一定存在
void listen(){
System.out.println(color); //可访问内部类的静态属性
System.out.println("听");
}
}
}
输出结果如下
瓜子脸
呼吸
Red
听
从结果我们可以看出:
内部类既可以访问自身的数据域,也可以访问他的外围类对象的数据域(如:Face.this.type)
编写内部类构造器这一方面,可以使用语法
outerObject.new InnerClass(Construction parm); 例如 Face.Nose n = f.new Nose();
特别注意:内部类不能有static方法
局部类不能用public或private访问说明符进行,它的作用域被限制在声明这个局部类的块中-->优势:对外部世界可以完全隐藏起来
匿名内部类
将局部内部类进一步深入,假如只创建这个类的一个对象,就不必命名了,这种类被称为“匿名内部类”
详细用法-->Click
包装类
java中的数据类型int,double等不是对象,无法通过向上转型获取到Object提供的方法,而像String却可以,只因为String是一个对象而不是一个类型。基本数据类型由于这样的特性,导致无法参与转型,泛型,反射等过程。为了弥补这个缺陷,java提供了包装类。
具体内容Click->here
常用类
Date()类
public class TestDate {
public static void main(String[] args) {
Date d = new Date(2000);
System.out.println(d);//Thu Jan 01 08:00:02 CST 1970
System.out.println(d.getTime());//2000
Date d2 = new Date();
System.out.println(d2.getTime());//1569572482084
System.out.println(d2.after(d));//true
}
}
Calendar类
public class TestCalendar {
public static void main(String[] args) {
//获得日期相关元素
Calendar calendar = new GregorianCalendar(2999,10,9,22,10,50);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH); //获得月份
int weekday = calendar.get(Calendar.DAY_OF_WEEK); //星期几
//System.out.println(year);
//System.out.println(month); //0-11表示对应的月份,0是1月...
//System.out.println(weekday); //1-7 1->星期天
//设置日期相关元素
Calendar calendar2 = new GregorianCalendar();
//System.out.println(calendar2); //不传参数,默认打印出今天的日期
calendar2.set(Calendar.YEAR,2020);
//System.out.println(calendar2);
//日期的计算
Calendar calendar3 = new GregorianCalendar();
calendar3.add(Calendar.YEAR,-100);//将该时间向前100年,第一个参数控制加减的是年/月/日,第二参数控制加减的大小
//System.out.println(calendar3);
//日期对象和时间对象的转换
Date date = calendar3.getTime();
Calendar calendar4 = new GregorianCalendar();
calendar4.setTime(new Date());
printCalendar(calendar4);
}
//打印日期的方法进行封装
private static void printCalendar(Calendar calendar){
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1;
int date = calendar.get(Calendar.DAY_OF_MONTH);
int dayWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1;
String dayWeek2 = dayWeek == 0 ? "日" :dayWeek + ""; //对星期进行处理
int hour = calendar.get(Calendar.HOUR);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
System.out.println(year + "年" + month + "月" + date + "日" + hour + "时" + minute + "分" + second + "秒"
+ " 周" + dayWeek2);
}
}
DateFormat类
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @Author: Rick
* @Date: 2019/1/28 18:38
* @Description: 测试时间对象和字符串之间的互相转换
* DateFormat抽象类的SimpleDateFormat实现类的使用
*/
public class TestDateFormat {
public static void main(String[] args) throws ParseException {
//把时间对象按照“格式字符串指定的格式”转成相应的字符串
DateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); // SimpleDateFormat()是DateFormat()的子类
String str = df.format(new Date(4000000));
System.out.println(str); // 打印已经转化成了的字符串 1970-01-01 09:06:40
//把字符串按照“格式字符串指定的格式”转成相应的时间对象
DateFormat df2 = new SimpleDateFormat("yyyy年MM月dd日hh时mm分ss秒");
Date date = df2.parse("2019年01月28日18时54分40秒");
System.out.println(date); //打印字符串转换成的日期 Mon Jan 28 18:54:40 CST 2019
//测试其他的格式字符。比如:利用D,获得本时间对象是所处年份的第几天
DateFormat df3 = new SimpleDateFormat("D");
String str3 = df3.format(new Date());
System.out.println(str3); //29
}
}
Math类
public class TestMath {
public static void main(String[] args) {
//取整相关操作
System.out.println(Math.ceil(3.2)); //4.0
System.out.println(Math.floor(3.2)); //3.0
System.out.println(Math.round(3.2)); //3
System.out.println(Math.round(3.8)); //4
//绝对值、开方、幂的操作
System.out.println(Math.abs(-45)); //45
System.out.println(Math.sqrt(64)); //8.0
System.out.println(Math.pow(5,2)); //25.0
System.out.println(Math.pow(2,5)); //32.0
//常量
System.out.println(Math.PI); //3.141592653589793
System.out.println(Math.E); //2.718281828459045
//随机数
System.out.println(Math.random()); // [0,1)之间的随机数
}
}
Random类
public class TestRandom {
public static void main(String[] args) {
Random rand = new Random();
System.out.println(rand.nextDouble()); //随机生成[0,1)之间的double类型的数据
System.out.println(rand.nextInt()); //随机生成int类型允许范围之内的整型数据
System.out.println(rand.nextFloat()); //随机生成[0,1)之间的float类型的数据
System.out.println(rand.nextBoolean()); //随机生成false或true
System.out.println(rand.nextInt(10)); //随机生成[0,10)之间的int类型的数据
System.out.println(20 + rand.nextInt(10)); //随机生成[20,30)之间的int类型的数据
System.out.println(20 + (int)(rand.nextDouble() * 10)); //随机生成[20,30)之间的int类型的数据,较为复杂
}
}
String、StringBuffer、StringBuilder
String常见问题
-
String长度为什么不可变?
简单的来说:String类中使用final关键字字符数组保存字符串 private final char value[],所以String对象是不可以变的
StringBuffer和StringBuilder
StringBuffer和Stringbuilder都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串 char [] value但是没有用final关键字修饰,所以这两种对象都是可变的
三者之间的比较
1.线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。
AbstractStringBuilder是StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。
StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全
2.性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
三者使用的总结
操作少量的数据 = String
单线程 操作字符串缓冲区下操作大量数据 = StringBuilder
多线程 操作字符串缓冲区下操作大量数据 = StringBuffer
Java中的传参机制
首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。 它用来描述各种程序设计语言(不只是Java)中方法参数传递方式。
Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。
参考文章(一 为什么 Java 中只有值传递?) ---> Click
Error、Exception、RuntimeException
三者联系
首先搞清楚他们三者之间的关系,Error和Exception都继承自Throwable,而RuntimeException继承Exception。在Java中只有Throwable类型的实例才可被抛出或捕获
Error类层次结构描述了Java运行时系统内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象。
在设计Java程序时,需要关注Exception的层次结构。这个层次结构又分解为两个分支:一个分支派生于RuntimeException;另一个分支包含其他异常。程序错误导致的异常属于RuntimeException;而程序本身没有问题,但像I/O错误这类问题导致的异常属于其他异常。
其中常见的RuntimeException有:
- NullPointerException:见的最多了,其实很简单,一般都是在null对象上调用方法了
- NumberFormatException:继承IllegalArgumentException,字符串转换为数字时出现。比如int i= Integer.parseInt("ab3");
- ArrayIndexOutOfBoundsException:数组越界。比如 int[] a=new int[3]; int b=a[3];
- StringIndexOutOfBoundsException:字符串越界。比如 String s="hello"; char c=s.chatAt(6);
- ClassCastException:类型转换错误。比如 Object obj=new Object(); String s=(String)obj;
- ArithmeticException:算术错误,典型的就是0作为除数的时候。
- IllegalArgumentException:非法参数,在把字符串转换成数字的时候经常出现的一个异常,我们可以在自己的程序中好好利用这个异常。
Error类或RuntimeException类的所有异常称为非受查异常,所有其他的异常称为受查异常
如何声明异常
如果遇到了无法处理的情况,那么Java方法可以抛出异常。
例如:
public FileInputStream(String name) throws FileNotFoundException
这个声明表示这个构造器将根据给定的String参数产生一个FileInputStream对象,但也有可能抛出一个FileNotFoundException异常。如果发生了这种糟糕的情况,构造器将不会初始化一个新的FileInputStream对象,而是抛出一个FileNotFoundException类对象。如果这个方法真的抛出了这样一个异常对象,运行时系统就会开始搜索异常处理器,以便知道如何处理FileNotFoundException对象。
什么时候使用throws子句声明异常?通常有以下几种情况
- 调用一个受查异常的方法,例如FileInputStream构造器
- 程序运行过程中发生错误
- 程序出现错误
- Java虚拟机和运行时库出现内部错误
警告 如果在子类中覆盖了一个超类的方法,子类方法中声明的受查异常不能比超类方法中声明的异常更加通用(也就是说:子类方法中可以抛出更特定的异常,或者根本不抛出任何异常)
如何抛出异常
首先看一个例子
String readData(Scanner in) throws EOFException
{
...
while(...)
{
if(!in.hasNext())
{
if(n < len)
throw new EOFException();
}
...
}
return s;
}
因此我们可以看到,对于一个已经存在的异常类,将其抛出很容易
1.找到一个合适的异常类
2.创建这个类的一个对象
3.将对象抛出
throw和throws关键字对比
1、throw用在方法体内,上面代码显示了,是直接在main方法体内,throws用在方法声明后面,表示再抛出异常,由该方法的调用者来处理。
2、throw是具体向外抛异常的,抛出的是一个异常实例,throws声明了是哪种类型的异常,使它的调用者可以捕获这个异常
3、throw如果执行了,那么一定是抛出了某种异常了,throws表示可能出现,但不一定抛出了异常。
4、同时出现的时候,throws出现在函数头、throw出现在函数体,两种不会由函数去处理,真正的处理由函数的上层调用处理
自定义异常
2种自定义异常的方法
/**
* @Author: Rick
* @Date: 2019/1/30 17:30
* @Description: 自定义异常方式
*/
public class Test02 {
public static void main(String[] args) {
Person p = new Person();
p.setAge(-10);
}
}
class Person{
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
if(age < 0){
try {
throw new IllegalException("年龄不能为负");
} catch (IllegalException e) {
e.printStackTrace();
}
}
this.age = age;
}
}
//自己定义异常
// 方式1继承RuntimeException,可以不用在代码中try/catch进行异常处理,遇到异常,程序中断
class IllegalException extends RuntimeException{
public IllegalException(){
}
public IllegalException(String msg){
super(msg);
}
}
//方式2继承Exception,必须在代码中对异常进行try/catch处理,这样才能通过编译
class IllegalException extends Exception {
public IllegalException() {
}
public IllegalException(String msg) {
super(msg);
}
}
结果如下:
Rick_03_Exception.IllegalException: 年龄不能为负
at Rick_03_Exception.Person.setAge(Test02.java:25)
at Rick_03_Exception.Test02.main(Test02.java:11)
Process finished with exit code 0
NoClassDefFoundError和ClassNotFoundException有什么区别?
NoClassDefFoundError是当Java虚拟机或ClassLoader实例试图加载某个类,但无法找到该类的定义时抛出此异常
ClassNotFoundException是当应用程序试图调用Class.forName(String)通过字符串名加载类,而找不到该类定义时抛出的异常。
equals()和==的区别
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)。
equals() : equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断。请看Object类中equals方法的源码:
public boolean equals(Object obj) {
return (this == obj);
}
我们来看一个例子:
public class Test01 {
public static void main(String[] args) {
Person p1 = new Person("Rick",1001);
Person p2 = new Person("Rick", 1001);
System.out.println(p1.equals(p2)); //false
}
}
class Person{
String name;
int ID;
public Person() {
}
public Person(String name, int ID) {
this.name = name;
this.ID = ID;
}
}
这里由于没有重写继承java.lang.Object类中的equals()方法,我们比较的是这两个对象的地址(==方法)因此出现的是false
我们再重写equals()方法看下测试结果:
public class Test02 {
public static void main(String[] args) {
Student stu1 = new Student("Rick",20);
Student stu2 = new Student("Rick",20);
System.out.println(stu1.equals(stu2)); //true
System.out.println(stu1 == stu2); //false
}
}
class Student{
String name;
int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
/**
* 重写equals()方法
* @param obj
* @return
*/
@Override
public boolean equals(Object obj) {
if (obj == null)
return false;
if (this == obj)
return true;
if (this.getClass() != obj.getClass())
return false;
Student stu = (Student)obj;
return (name.equals(stu.name) && age == stu.age);
}
}
实际上我们再String类型中使用的equals()方法,已经是被重写过了-->参考博文
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
在这里我们特别注意创建字符串String时
String s="abce"是一种非常特殊的形式,new 有本质的区别。它是java中唯一不需要new 就可以产生对象的途径。以String s="abce"形式赋值在java中叫直接量它是在常量池中而不是像new一样放在压缩堆中。这种形式的字符串,在JVM内部发生字符串拘留,即当声明这样的一个字符串后,JVM会在常量池中先查找有有没有一个值为"abcd"的对象,如果有,就会把它赋给当前引用.即原来那个引用和现在这个引用指点向了同一对象,如果没有,则在常量池中新创建一个"abcd",下一次如果有String s1 = "abcd";又会将s1指向"abcd"这个对象,即以这形式声明的字符串,只要值相等,任何多个引用都指向同一对象.
而String s = new String("abcd");和其它任何对象一样.每调用一次就产生一个对象,只要它们调用。
HashCode()和equals()区别
推荐阅读:Java hashCode() 和 equals()的若干问题解答
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数 。这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。
虽然,每个Java类都包含hashCode() 函数。但是,仅仅当创建并某个“类的散列表”(关于“散列表”见下面说明)时,该类的hashCode() 才有用(作用是:确定该类的每一个对象在散列表中的位置;其它情况下(例如,创建类的单个对象,或者创建类的对象数组等等),类的hashCode() 没有作用。
上面的散列表,指的是:Java集合中本质是散列表的类,如HashMap,Hashtable,HashSet。
HashCode()和equals()的关系
我们以 类的用途 来将“hashCode() 和 equals()的关系”分2种情况来说明。
不会创建“类对应的散列表”
这里所说的“不会创建类对应的散列表”是说:我们不会在HashSet, Hashtable, HashMap等等这些本质是散列表的数据结构中,用到该类。例如,不会创建该类的HashSet集合。在这种情况下,该类的“hashCode() 和 equals() ”没有半毛钱关系的!
import java.util.*;
import java.lang.Comparable;
/**
* @desc 比较equals() 返回true 以及 返回false时, hashCode()的值。
*
* @author skywang
* @emai kuiwu-wang@163.com
*/
public class NormalHashCodeTest{
public static void main(String[] args) {
// 新建2个相同内容的Person对象,
// 再用equals比较它们是否相等
Person p1 = new Person("eee", 100);
Person p2 = new Person("eee", 100);
Person p3 = new Person("aaa", 200);
System.out.printf("p1.equals(p2) : %s; p1(%d) p2(%d)\n", p1.equals(p2), p1.hashCode(), p2.hashCode());
System.out.printf("p1.equals(p3) : %s; p1(%d) p3(%d)\n", p1.equals(p3), p1.hashCode(), p3.hashCode());
}
/**
* @desc Person类。
*/
private static class Person {
int age;
String name;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " - " +age;
}
/**
* @desc 覆盖equals方法
*/
public boolean equals(Object obj){
if(obj == null){
return false;
}
//如果是同一个对象返回true,反之返回false
if(this == obj){
return true;
}
//判断是否类型相同
if(this.getClass() != obj.getClass()){
return false;
}
Person person = (Person)obj;
return name.equals(person.name) && age==person.age;
}
}
}
得到结果如下:
p1.equals(p2) : true; p1(1169863946) p2(1901116749)
p1.equals(p3) : false; p1(1169863946) p3(2131949076)
从结果也可以看出:p1和p2相等的情况下,hashCode()也不一定相等。
会创建“类对应的散列表”
这里所说的“会创建类对应的散列表”是说:我们会在HashSet, Hashtable, HashMap等等这些本质是散列表的数据结构中,用到该类。例如,会创建该类的HashSet集合。
在这种情况下,该类的“hashCode() 和 equals() ”是有关系的:
1)、如果两个对象相等,那么它们的hashCode()值一定相同。
这里的相等是指,通过equals()比较两个对象时返回true。
2)、如果两个对象hashCode()相等,它们并不一定相等。
因为在散列表中,hashCode()相等,即两个键值对的哈希值相等。然而哈希值相等,并不一定能得出键值对相等。补充说一句:“两个不同的键值对,哈希值相等”,这就是哈希冲突
我们来看一个例子:
import java.util.*;
import java.lang.Comparable;
/**
* @desc 比较equals() 返回true 以及 返回false时, hashCode()的值。
*
* @author skywang
* @emai kuiwu-wang@163.com
*/
public class ConflictHashCodeTest1{
public static void main(String[] args) {
// 新建Person对象,
Person p1 = new Person("eee", 100);
Person p2 = new Person("eee", 100);
Person p3 = new Person("aaa", 200);
// 新建HashSet对象
HashSet set = new HashSet();
set.add(p1);
set.add(p2);
set.add(p3);
// 比较p1 和 p2, 并打印它们的hashCode()
System.out.printf("p1.equals(p2) : %s; p1(%d) p2(%d)\n", p1.equals(p2), p1.hashCode(), p2.hashCode());
// 打印set
System.out.printf("set:%s\n", set);
}
/**
* @desc Person类。
*/
private static class Person {
int age;
String name;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return "("+name + ", " +age+")";
}
/**
* @desc 覆盖equals方法
*/
@Override
public boolean equals(Object obj){
if(obj == null){
return false;
}
//如果是同一个对象返回true,反之返回false
if(this == obj){
return true;
}
//判断是否类型相同
if(this.getClass() != obj.getClass()){
return false;
}
Person person = (Person)obj;
return name.equals(person.name) && age==person.age;
}
}
}
我们看到结果:
p1.equals(p2) : true; p1(1169863946) p2(1690552137)
set:[(eee, 100), (eee, 100), (aaa, 200)]
结果分析:
我们重写了Person的equals()。但是,很奇怪的发现:HashSet中仍然有重复元素:p1 和 p2。为什么会出现这种情况呢?
这是因为虽然p1 和 p2的内容相等,但是它们的hashCode()不等;所以,HashSet在添加p1和p2的时候,认为它们不相等。
下面我们同时覆盖equals() 和 hashCode()方法
import java.util.*;
import java.lang.Comparable;
/**
* @desc 比较equals() 返回true 以及 返回false时, hashCode()的值。
*
* @author skywang
* @emai kuiwu-wang@163.com
*/
public class ConflictHashCodeTest2{
public static void main(String[] args) {
// 新建Person对象,
Person p1 = new Person("eee", 100);
Person p2 = new Person("eee", 100);
Person p3 = new Person("aaa", 200);
Person p4 = new Person("EEE", 100);
// 新建HashSet对象
HashSet set = new HashSet();
set.add(p1);
set.add(p2);
set.add(p3);
// 比较p1 和 p2, 并打印它们的hashCode()
System.out.printf("p1.equals(p2) : %s; p1(%d) p2(%d)\n", p1.equals(p2), p1.hashCode(), p2.hashCode());
// 比较p1 和 p4, 并打印它们的hashCode()
System.out.printf("p1.equals(p4) : %s; p1(%d) p4(%d)\n", p1.equals(p4), p1.hashCode(), p4.hashCode());
// 打印set
System.out.printf("set:%s\n", set);
}
/**
* @desc Person类。
*/
private static class Person {
int age;
String name;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " - " +age;
}
/**
* @desc重写hashCode
*/
@Override
public int hashCode(){
int nameHash = name.toUpperCase().hashCode();
return nameHash ^ age;
}
/**
* @desc 覆盖equals方法
*/
@Override
public boolean equals(Object obj){
if(obj == null){
return false;
}
//如果是同一个对象返回true,反之返回false
if(this == obj){
return true;
}
//判断是否类型相同
if(this.getClass() != obj.getClass()){
return false;
}
Person person = (Person)obj;
return name.equals(person.name) && age==person.age;
}
}
}
运行结果
p1.equals(p2) : true; p1(68545) p2(68545)
p1.equals(p4) : false; p1(68545) p4(68545)
set:[aaa - 200, eee - 100]
结果分析:
这下,equals()生效了,HashSet中没有重复元素。
比较p1和p2,我们发现:它们的hashCode()相等,通过equals()比较它们也返回true。所以,p1和p2被视为相等。
比较p1和p4,我们发现:虽然它们的hashCode()相等;但是,通过equals()比较它们返回false。所以,p1和p4被视为不相等。