目录
- java基础知识
- 1、JDK 和 JRE 有什么区别?
- 2、基本数据类型
- 包装类型的缓存机制了解么?
- 自动装箱与拆箱了解吗?原理是什么?
- 如何解决浮点数运算的精度丢失问题?
- 超过 long 整型的数据应该如何表示?
- 3、Java的四种访问修饰符
- 面向对象三大特征
- 重载和重写有什么区别?
- 接口和抽象类有什么共同点和区别?
- java 8的新特性
- 深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
- 4、== 和 equals 的区别是什么?
- hashCode() 有什么用?
- String 字符串
- 三元运算符号
- 简述synchronized 和java.util.concurrent.locks.Lock的异同
- 静态方法为什么不能调用非静态成员?
- 什么是可变长参数?
- 进制转换
- 二维数组
- 日历类
java基础知识
1、JDK 和 JRE 有什么区别?
一、概念
JDK:Java Development Kit
的简称,java 开发工具包,提供了 java 的 开发环境 和 运行环境,是程序员使用java语言编写java程序所需的开发工具包。
JRE:Java Runtime Environment
的简称,java 运行环境,为 java 的 运行 提供了所需环境,包含了java虚拟机,java基础类库,是使用java语言编写的程序运行所需要的软件环境。
看一张图:
从图中可以看出JDK是整个JAVA的核心,包括了Java运行环境JRE(Java Runtime Envirnment)
、一堆Java开发工具(javac/java/jdb等)
和Java基础的类库(即Java API 包)
。
二、区别
1、面向人群不同
JDK是提供给程序员
使用的。JRE是提供给想运行java程序的用户
使用的。
2、重要程度不同
如果你需要编写java程序,需要安装JDK。如果你需要运行java程序,只需要安装JRE就可以了。
jdk包含jre,但是jre可单独运行。
2、基本数据类型
1、概念
Java数据类型分为两大类,基本数据类型,引用类型
数据类型名称 | 占用字节 | 取值范围 | 默认值 | 封装类 |
---|---|---|---|---|
byte (字节型) | 1 | -2^7 — 2^7-1(-128 ~ 127) | 0 | Byte |
short (短整型) | 2 | -2^15 — 2^15-1 | 0 | Short |
int (整型) | 4 | -2^31 — 2^31-1 | 0 | Integer |
long (长整型) | 8 | -2^63 — 2^63-1 | 0l | Long |
float (单精度浮点型) | 4 | -2^31 — 2^31-1 | 0.0f | Float |
double (双精度浮点型) | 8 | -2^63 — 2^63-1 | 0 | Double |
char (字符型) | 2 | -2^15 — 2^15-1 | \u0000(空格) | Character |
boolean (布尔型) | 1 | false | Boolean |
一个字节等于八位(1byte = 8bit)
float 指的是单精度变量,尾数可以有7位数,精确度较低。
double指的是双精度变量。尾数是float的二倍,所以常用double。
2、基本类型和包装类的异同
首先几乎在所有位置,long的小写和大写都可以互相替换。其次L本质是对象,不是基础类型,具有Object的特性。
包装类把基本类型转换为对象,每个基本类型在java.lang包中都有一个相应的包装类,如上表:
- 在Java中,一切皆对象,但八大基本类型却不是对象。
- 包装类型可用于泛型,而基本类型不可以。
- 声明方式的不同,基本类型无需通过new关键字来创建,而封装类型需new关键字。
- 存储方式及位置的不同,基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
- 初始值的不同,封装类型的初始值为null,基本类型的的初始值视具体的类型而定,比如int类型的初始值为0,boolean类型为false;
- 使用方式的不同:如与集合类合作使用时只能使用包装类型。
- long类型最好以大写L来添加尾缀,因为小写l容易和数字1混淆。
3、基本类型之间的转换
将一种类型的值赋值给另一种类型是很常见的。在Java中,boolean 类型与其他7中类型的数据都不能进行转换,这一点很明确。但对于其他7种数据类型,它们之间都可以进行转换,只是可能会存在精度损失或其他一些变化。
转换分为自动转换和强制转换:
- 自动转换(隐式):无需任何操作。
- 强制转换(显式):需使用转换操作符(type)。
将6种数据类型按下面顺序排列一下:
double > float > long > int > short > byte
如果从小转换到大,那么可以直接转换,
从大到小,或 char 和其他6种数据类型转换,则必须使用强制转换。
正常写法:
long lNum = 1234L;float fNum = 1.23f;double dNum = 1.23d;
说明一:
Java语言中,整数直接量(例如:1、2、10等),JVM虚拟机是默认为int类型数据
的。所以,当整数直接量赋给long、float或者double,而不添加尾缀,虚拟机也会直接将int类型数据自动转换为对应类型然后赋值。也就是小类型转大类型
并不会造成数据丢失,所以默认可以自动转换。
long lNum = 5; //不报错,因为int自动转换为long类型,不会报错
float fNum = 7; //不报错,因为int自动转换为float类型,不会报错
double dNum = 10; //同上
但是,当浮点直接量(例如:1.2等),JVM虚拟机默认为double类型,如果直接赋值给float就会引起编译器报错。
float fNum = 1.2; //报错,因为1.2虚拟机是默认为double类型,不能直接赋值给float类型变量
float fNew = 1.3f;//正确,因为尾缀添加了f,即告诉了虚拟机1.3属于float类型变量
long lNum = 1.2L; //错误,double类型数据不能直接赋值给long类型
long lNew = (long)1.2; //正确,double类型数据强制转换为long类型
特例:
1、对于short s1 = 1; s1 = s1 + 1;由于1是int类型,因此s1+1运算结果也是int 型,需要强制转换类型才能赋值给short型。
正确的写法是:short s1 = 1; s1 = (short)(s1 + 1)
2、short s1 = 1; s1 += 1 可否编译成功
可以正确编译,因为s1 += 1;相当于s1 = (short)(s1 + 1);其中有隐含的强制类型转换。
再看看其他说明:
public class FloatDetail {
public static void main(String[] args) {
//Java 浮点类型常量(具体值)默认为double型,声明float型常量,须后加'f'或'F'
//float num1=1.1; //错误
float num2 = 1.1F; //对的
double num3 = 1.1; //对的
double num4 = 1.1f; //对的
//十进制数形式:如:5.12 512.0f .512(必须有小数点)
double num5 = .123; //等价0.123
System.out.println(num5); //0.123
//科学计数法形式:如:5.12e2[5.12*10的2次方] 5.12E-2[5.12/10的2次方]
System.out.println(5.12e2); //512.0
System.out.println(5.12E-2); //0.0512
//通常情况下,应该使用double类型,因为它比float型更精确
double num9 = 2.1234567851;
float num10 = 2.1234567851f;
System.out.println(num9); //输出: 2.1234567851
System.out.println(num10); //输出: 2.1234567
//浮点数使用陷阱:2.7 和 8.1/3 比较
//看一看代码
double num11 = 2.7;
double num12 = 8.1 / 3; //2.7
System.out.println(num11);//2.7
System.out.println(num12); //输出接近2.7的小数 2.6999999999999997
//得到一个重要的使用点:当我们对运算结果是小数的进行相等判断时,要小心
//应该是以两个数的差值的绝对值,在某个精度范围内判断
if (num11 == num12) { //false
System.out.println("相等");
}
//正确的写法
if (Math.abs(num11 - num12) < 0.000001) {
System.out.println("差值非常小,到我的规定精度,认为相等...");
}
System.out.println(Math.abs(num11 - num12)); //4.440892098500626E-16
//细节:如果是直接查询得到的小数,或者是直接赋值,是可以判断相等的
double num13 = 2.7;
double num14 = 2.7;
if (num13 == num14) { //true
System.out.println("相等");
}
}
}
字符类型
char 常用单引号放字符
char aa ='a';
char aa ='ab';//这个则报错,只能放一个
char ='\n' //换行符
char='\t' //制表符 相当于Tab键
-
基本介绍:
字符类型可以表示单个字符,字符类型是char,char是两个字节(可以存放汉字),多个字符我们用字符串Stringpublic class Char01 { public static void main(String[] args) { char c1 = 'a'; char c2 = '\t'; char c3 = '路'; char c4 = 97; //说明:字符类型可以直接存放一个数字 System.out.println(c1); System.out.println(c2); System.out.println(c3); System.out.println(c4); //当输出c4的时候,会输出97表示的字符 =>编码的概念 } }
-
字符类型使用细节:
1、字符常量是用单引号( ’ ’ )括起来的单个字符。例如:char c1 = ‘a’; char c2 = ‘中’; char c3 = ‘9’;
2、java中还允许使用转义符 ‘’ 来将其后的字符串转变为特殊字符型常量。例如: char c3 = ‘\n’; 其中 '\n’表示换行符
3、在java中,char的本质是一个整数,在输出时,是unicode
码对应的字符。
http://tool.chinaz.com/Tools/Unicode.aspx(转化地址)
4、可以直接给char赋一个整数,然后输出时,会按照对应的 unicode 字符输出[97]
5、char类型是一个单一的 16 位 Unicode 字符;
char 类型是可以进行运算的,相当于一个整数,因为它都对应有Unicode码。public class CharDetail { public static void main(String[] args) { //在java中,char的本质是一个整数,在默认输出时,是Unicode码对应的字符 //要输出对应的数字,可以(int)字符 char c1 = 97; System.out.println(c1); //a char c2 = 'a'; //输出'a' 对应的数字 System.out.println((int) c2); //97 char c3 = '路'; System.out.println((int) c3);//36335 char c4 = 36335; System.out.println(c4); //路 //char 类型是可以进行运算的,相当于一个整数,因为它都有对应的Unicode码 System.out.println('a' + 10); //107 //测试 char c5 = 'b' + 1; //98+1 =>99 System.out.println((int) c5); //99 System.out.println(c5); //99 -> 对应的字符-> 编码表ASCII(规定好的)=>c } }
-
字符类型本质
1、字符型 存储到 计算机中,需要将字符对应的码值(整数)找出来,比如:‘a’
储存:‘a’ ==> 码值 97 > 二进制(110 0001)> 存储
读取:二进制(110 0001) => 97 ===> ‘a’ => 显示
2、字符和码值的对应关系是通过字符编码表决定的(是规定好的)
介绍一下字符编码表[sublime测试]
-
ASCII
(ASCII 编码表 一个字节表示,一个128个字符,实际上一个字节可以表示256个字符,只用128个) ASCII对照表:http://ascii.wjccx.com/ -
Unicode
(Unicode 编码表 固定大小的编码 使用两个字节来表示字符,字母和汉字统一都是占用两个字节,这样浪费空间) -
utf-8
(编码表,大小可变的编码,字母、数字、符号占用1个字节,中文汉字使用3个字节) -
gbk
(可以表示汉字,而且范围广,字母使用1个字节,汉字使用2个字节) -
gbk2312
(可以表示汉字,gbk2312 < gbk) -
big5
码(繁体中文,台湾,香港)
包装类型的缓存机制了解么?
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte,Short,Integer,Long
这 4 种包装类默认创建了数值 [-128,127]
的相应类型的缓存数据,Character 创建了数值在 [0,127]
范围的缓存数据,Boolean 直接返回 True or False
。
Integer 缓存源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static {
// high value may be configured by property
int h = 127;
}
}
Character 缓存源码:
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}
private static class CharacterCache {
private CharacterCache(){}
static final Character cache[] = new Character[127 + 1];
static {
for (int i = 0; i < cache.length; i++)
cache[i] = new Character((char)i);
}
}
Boolean 缓存源码:
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。
两种浮点数类型的包装类 Float
,Double
并没有
实现缓存机制。
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true
Float i11 = 333f;
Float i22 = 333f;
System.out.println(i11 == i22);// 输出 false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false
下面我们来看一下问题。下面的代码的输出结果是 true 还是 false 呢?
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);
Integer i1=40 这一行代码会发生装箱,也就是说这行代码等价于 Integer i1=Integer.valueOf(40) 。因此,i1 直接使用的是缓存中的对象。而Integer i2 = new Integer(40) 会直接创建新的对象。
因此,答案是 false 。你答对了吗?
记住:所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
自动装箱与拆箱了解吗?原理是什么?
什么是自动拆装箱?
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
举例:
Integer i = 10; //装箱
int n = i; //拆箱
上面这两行代码对应的字节码为:
L1
LINENUMBER 8 L1
ALOAD 0
BIPUSH 10
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
PUTFIELD AutoBoxTest.i : Ljava/lang/Integer;
L2
LINENUMBER 9 L2
ALOAD 0
ALOAD 0
GETFIELD AutoBoxTest.i : Ljava/lang/Integer;
INVOKEVIRTUAL java/lang/Integer.intValue ()I
PUTFIELD AutoBoxTest.n : I
RETURN
从字节码中,我们发现装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。
因此,
- Integer i = 10 等价于 Integer i = Integer.valueOf(10)
- int n = i 等价于 int n = i.intValue();
注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
private static long sum() {
// 应该使用 long 而不是 Long
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
为什么浮点数运算的时候会有精度丢失的风险?
浮点数运算精度丢失代码演示:
float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false
为什么会出现这个问题呢?
这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。
就比如说十进制下的 0.2 就没办法精确转换成二进制小数:
// 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,
// 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0(发生循环)
...
如何解决浮点数运算的精度丢失问题?
BigDecimal
可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal
来做的。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
System.out.println(x); /* 0.1 */
System.out.println(y); /* 0.1 */
System.out.println(Objects.equals(x, y)); /* true */
超过 long 整型的数据应该如何表示?
基本数值类型都有一个表达范围,如果超过这个范围就会有数值溢出的风险。
在 Java 中,64 位 long 整型是最大的整数类型。
long l = Long.MAX_VALUE;
System.out.println(l + 1); // -9223372036854775808
System.out.println(l + 1 == Long.MIN_VALUE); // true
BigInteger 内部使用 int[] 数组来存储任意大小的整形数据。
相对于常规整数类型的运算来说,BigInteger 运算的效率会相对较低。
3、Java的四种访问修饰符
public(公共的)接口访问权限:使用public关键字,就意味着被声明的成员或方法对所有类都是可以访问的。
protected(受保护的)继承访问权限:使用protected关键字,就意味着被声明的成员或方法,在子类以及相同包内的其他类都是可以访问的。
default(默认的)包访问权限:即不写任何关键字,就意味着相同包内的其他类(包括子类)可以访问,包外都不可以访问。
private(私有的)无法访问:使用 private 关键字,就意味着被声明的成员或方法,除了本类,其他任何类都无法访问。
问题一、为什么不能用private修饰Java外部类?
因为如果使用private修饰Java外部类,那么这个类不能创建实例,这个类的属性和方法不能被访问,那么创建这个类毫无意义,所以不能使用private修饰Java外部类。
问题二、为什么不能用protected修饰Java外部类?
举个栗子,如果类A用 protected 修饰,与类A不同包的类B想要访问类A的话,类B就必须是继承类A的(或者说类B必须为类A的子类),但是类B继承类A的前提又是类B可以访问到类A,仔细想想会发现这里是冲突的,其实这就说明了为什么不能用protected来修饰外部类。
面向对象三大特征
-
封装
封装是指把一个 对象的属性 隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。public class Student { private int id;//id属性私有化 private String name;//name属性私有化 //获取id的方法 public int getId() { return id; } //设置id的方法 public void setId(int id) { this.id = id; } //获取name的方法 public String getName() { return name; } //设置name的方法 public void setName(String name) { this.name = name; } }
2、继承
https://blog.csdn.net/weixin_45080272/article/details/129587150
3、多态
多态的概念
-
多态
是方法
或对象
具有多种形态,是面向对象的第三大特征,其中对象的多态
是多态的核心和重点。 -
多态
的前提是两个对象(类)存在继承
关系,多态是建立在封装
和继承
基础之上的。多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
多态,就是方法或对象有多种形态,包括方法的多态
和对象的多态
。- 方法的多态
重载和重写有什么区别?
重载:
发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
重写:
父类方法中的功能无法满足子类的要求,所以对父类中的方法进行重写。
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
- 1、
方法名
、参数列表
必须相同,子类方法返回值类型
应比父类方法返回值类型更小或相等,抛出的异常
范围小于等于父类,访问修饰符范围大于等于父类。 - 2、如果父类方法访问修饰符为
private/final/static
则子类就不能
重写该方法,但是被 static 修饰的方法能够被再次声明。 - 3、
构造方法
无法被重写
综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。
方法重载
是一个类
的多态性的表现,而方法重写
是子类与父类
的一种多态性表现
方法的重写要遵循“两同两小一大
”:
- “两同”即方法名相同、形参列表相同;
- “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
- “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。
关于 重写的返回值类型 这里需要额外多说明一下,上面的表述不太清晰准确:
- 如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。
- 但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。
public class Hero {
public String name() {
return "超级英雄";
}
}
public class SuperMan extends Hero{
@Override
public String name() {
return "超人";
}
public Hero hero() {
return new Hero();
}
}
public class SuperSuperMan extends SuperMan {
public String name() {
return "超级超级英雄";
}
@Override
public SuperMan hero() {
return new SuperMan();
}
}
接口和抽象类有什么共同点和区别?
共同点 :
- 都不能被实例化。
- 都可以包含抽象方法。
- 都可以有默认实现的方法(Java 8 新特性:接口中可以添加default修饰的非抽象方法,可以有方法体和内容)。
区别
- 接口只有方法定义,没有具体的实现,实现接口的类要实现接口的所有方法;抽象类可以有定义与实现;
- 接口与类是实现关系,并且类可以多实现;抽象类与类是继承关系,只能被一个类继承。
- 接口中成员全为public 抽象类中可以有抽象方法,也可以没有,抽象方法需加abstract
- 接口中无静态方法,抽象类中可以有。
- 接口中不能定义构造器,抽象类中可以。
java 8的新特性
-
1、接口中可以添加
default
修饰的非抽象方法,可以有方法体和内容。default
修饰的方法,是普通实例方法,可以用this
调用,可以被子类继承、重写。static
修饰的方法,使用上和一般类静态方法一样。但它不能被子类继承,只能用Interface
调用。
-
②可以使用
lambda
表达式,减少代码冗余。 -
③函数式接口,使用
@FunctionalInterface
注解标明,该接口有且仅有一个抽象方法。 -
④方法引用,可以直接引用已有
Java
类或对象的方法或构造器,进一步简化lambda
表达式。 -
⑤stream流,用于解决已有集合/数组类库的弊端,简化其操作,有
foreach
遍历、filter
过滤、map
映射、concat
合并等功能。 -
⑥增加日期相关的
API
。
参考:https://mp.weixin.qq.com/s/ojyl7B6PiHaTWADqmUq2rw
深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
1、概述
Java
中的对象拷贝 ( Object Copy )
是指将一个对象的所有属性(成员变量)拷贝到另一个有着相同类类型的对象中去。
- 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
- 深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
2、浅拷贝
浅拷贝的示例代码如下,我们这里实现了 Cloneable
接口,并重写了 clone()
方法。
clone()
方法的实现很简单,直接调用的是父类 Object
的 clone()
方法。
public class Address implements Cloneable{
private String name;
// 省略构造函数、Getter&Setter方法
@Override
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class Person implements Cloneable {
private Address address;
// 省略构造函数、Getter&Setter方法
@Override
public Person clone() {
try {
Person person = (Person) super.clone();
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
测试 :
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// true
System.out.println(person1.getAddress() == person1Copy.getAddress());
从输出结构就可以看出, person1
的克隆对象和 person1
使用的仍然是同一个 Address
对象。
3、深拷贝
这里我们简单对 Person
类的 clone()
方法进行修改,连带着要把 Person
对象内部的 Address
对象一起复制。
@Override
public Person clone() {
try {
Person person = (Person) super.clone();
person.setAddress(person.getAddress().clone());
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
测试 :
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// false
System.out.println(person1.getAddress() == person1Copy.getAddress());
从输出结构就可以看出,虽然 person1 的克隆对象和 person1 包含的 Address 对象已经是不同的了。
4、那什么是引用拷贝呢? 简单来说,引用拷贝就是两个不同的引用指向同一个对象。
4、== 和 equals 的区别是什么?
1、== 的解读
对于基本类型和引用类型 == 的作用效果是不同的,如下所示:
- 基本类型:比较的是值是否相同;
- 引用类型:比较的是引用是否相同;
代码示例:
String x = "string";
String y = "string";
String z = new String("string");
System.out.println(x==y); // true
System.out.println(x==z); // false
System.out.println(x.equals(y)); // true
System.out.println(x.equals(z)); // true
代码解读:因为 x 和 y 指向的是同一个引用,所以 == 也是 true,而 new String()方法却重写开辟了内存空间,所以 == 结果为 false,而 equals 比较的一直是值,所以结果都为 true。
2、equals 解读
equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较。看下面的代码就明白了。
首先来看默认情况下 equals 比较一个有相同值的对象,代码如下:
class Cat {
public Cat(String name) {
this.name = name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Cat c1 = new Cat("小王");
Cat c2 = new Cat("小王");
System.out.println(c1.equals(c2)); // false
输出结果出乎我们的意料,竟然是 false?这是怎么回事,看了 equals 源码就知道了,源码如下:
public boolean equals(Object obj) {
return (this == obj);
}
原来 equals 本质上就是 ==。
问题:
两个相同值的 String 对象,为什么返回的是 true?代码如下:
String s1 = new String("王老五");
String s2 = new String("王老五");
System.out.println(s1.equals(s2)); // true
同样的,当我们进入 String 的 equals 方法,找到了答案,代码如下:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
原来是 String 重写了 Object 的 equals 方法,把引用比较改成了值比较。
总结 :
== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;
而 equals 默认情况下是引用比较,只是很多类重写了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。
hashCode() 有什么用?
hashCode()
的作用是获取哈希码(int 整数),也称为散列码
。这个哈希码的作用是确定该对象在哈希表中的索引位置
。
hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的,该方法通常用来将对象的内存地址转换为整数之后返回。
public native int hashCode();
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
为什么要有 hashCode?
我们以HashSet
如何检查重复”为例子来说明为什么要有 hashCode?
当你把对象加入
HashSet
时,HashSet 会先计算对象的hashCode
值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode
值作比较,如果没有相符的hashCode
,HashSet
会假设对象没有重复出现。但是如果发现有相同hashCode
值的对象,这时会调用equals()
方法来检查hashCode
相等的对象是否真的相同。
如果两者相同,HashSet
就不会让其加入操作成功。
如果不同的话,就会重新散列到其他位置。这样我们就大大减少了equals
的次数,相应就大大提高了执行速度。
其实, hashCode() 和 equals()都是用于比较两个对象是否相等。
那为什么 JDK 还要同时提供这两个方法呢?
这是因为在一些容器(比如 HashMap
、HashSet
)中,有了 hashCode()
之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HashSet的过程)!
我们在前面也提到了添加元素进HashSet的过程,如果 HashSet 在对比的时候,同样的 hashCode
有多个对象,它会继续使用 equals()
来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本。
那为什么不只提供 hashCode() 方法呢?
这是因为两个对象的hashCode
值相等并不代表两个对象就相等。
那为什么两个对象有相同的 hashCode 值,它们也不一定是相等的?
因为 hashCode()
所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode
)。
总结下来就是 :
- 如果两个对象的`hashCode` 值相等,那这两个对象不一定相等(哈希碰撞)。
- 如果两个对象的`hashCode` 值相等并且`equals()`方法也返回 `true`,我们才认为这两个对象相等。
- 如果两个对象的`hashCode` 值不相等,我们就可以直接认为这两个对象不相等。
相信大家看了我前面对 hashCode()
和 equals()
的介绍之后,下面这个问题已经难不倒你们了。
为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的 hashCode
值必须是相等。也就是说如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
如果重写 equals()
时没有重写 hashCode()
方法的话就可能会导致 equals
方法判断是相等的两个对象,hashCode
值却不相等。
总结 :
equals
方法判断两个对象是相等的,那这两个对象的hashCode
值也要相等。- 两个对象有相同的
hashCode
值,他们也不一定是相等的(哈希碰撞)。
参考地址:
https://www.cnblogs.com/skywang12345/p/3324958.html
String 字符串
https://blog.csdn.net/weixin_45080272/article/details/129440270
三元运算符号
-
基本语法
- 条件表达式 ? 表达式 1: 表达式 2;
- 运算规则:
- 如果条件表达式为 true,运算后的结果是表达式 1;
- 如果条件表达式为 false,运算后的结果是表达式 2;
-
其他写法
// 这里说明一点,只需要判断条件表达式的值 String ap = null, tu = "adf"; String ok = ap == null || tu == null ? "返回1的值" : "返回2的值"; System.out.println(ok); // 返回1的值
简述synchronized 和java.util.concurrent.locks.Lock的异同
1、从功能角度来看
Lock和Synchronized都是java中去用来解决线程安全问题的一个工具
-
区别1:
Synchronized
是Java的一个关键字
,而Lock
是java.util.concurrent.Locks包下的一个接口
; -
区别2:
Synchronized
使用过后,会自动释放锁
,而Lock
需要手动上锁
、手动释放锁
。(在 finally 块中) -
区别3:
Lock
提供了更多的实现方法,而且可响应中断
、可定时
, 而synchronized
关键字不能响应中断
;
看下 Lock 接口源码:package java.util.concurrent.locks; import java.util.concurrent.TimeUnit; public interface Lock { void lock(); // 获取锁 void lockInterruptibly() throws InterruptedException; //获取锁,可响应中断; boolean tryLock(); // 获取锁 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //在一定时间单位内等待后,尝试获取锁 void unlock(); Condition newCondition();
响应中断:
A、B 线程同时想获取到锁,A获取锁以后,B会进行等待,这时候等待着锁的线程B,会被Thread.interrupt()方法给中断等待状态、然后去执行其他的事情,而synchronized锁无法被Tread.interrupt()方法给中断掉;
ps:
可中断锁:在利用thread.interrupted监测到中断请求后,会抛出interruptedexception异常,进而中断线程的执行。 -
区别4:
synchronized
关键字是非公平锁
,即,不能保证等待锁的那些线程们的顺序,而Lock
的子类ReentrantLock
默认是非公平锁,但是可通过一个布尔参数的构造方法实例化出一个公平锁;
静态方法为什么不能调用非静态成员?
这个需要结合 JVM 的相关知识,主要原因如下:
- 1、静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
- 2、在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
什么是可变长参数?
从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面的这个 printVariable 方法就可以接受 0 个或者多个参数。
public static void method1(String... args) {
//......
}
另外,可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数。
public static void method2(String arg1, String... args) {
//......
}
遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢?
答案是会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。
我们通过下面这个例子来证明一下。
/**
* 微信搜 JavaGuide 回复"面试突击"即可免费领取个人原创的 Java 面试手册
*
* @author Guide哥
* @date 2021/12/13 16:52
**/
public class VariableLengthArgument {
public static void printVariable(String... args) {
for (String s : args) {
System.out.println(s);
}
}
public static void printVariable(String arg1, String arg2) {
System.out.println(arg1 + arg2);
}
public static void main(String[] args) {
printVariable("a", "b");
printVariable("a", "b", "c", "d");
}
}
输出:
ab
a
b
c
d
另外,Java 的可变参数编译后实际会被转换成一个数组,我们看编译后生成的 class文件就可以看出来了。
public class VariableLengthArgument {
public static void printVariable(String... args) {
String[] var1 = args;
int var2 = args.length;
for(int var3 = 0; var3 < var2; ++var3) {
String s = var1[var3];
System.out.println(s);
}
}
// ......
}
进制转换
二维数组
https://blog.csdn.net/weixin_45395059/article/details/125636450
日历类
https://blog.csdn.net/cnds123321/article/details/119335948