目录
7. 两个对象的 hashCode() 相同, 那么 equals() 也一定为 true 吗?
9. 为什么重写 equals时必须重写 hashCode 方法?
10. java里equals和hashCode之间什么关系
两个对象的 hashCode() 相同, 那么 equals() 也一定为 true吗?
1. Java 中的 Math. round(-1. 5) 等于多少?
3. Java 中操作字符串都有哪些类?它们之间有什么区别?
4. String str="i"与 String str=new String("i")一样吗?
5. String & StringBuffer & StringBuilder
6. String、StringBuffer、StringBuilder的区别
9. StringBuffer是线程安全,StringBuilder是非线程安全的。
11. String a = "123"; String b = "123"; a==b 吗?为什么??
9. 在 Java 中定义一个不做事且没有参数的构造 方法的作用
12. 创建一个对象用什么运算符? 对象实体与对象引用有何不同
14. 一个类的构造方法的作用是什么?若一个类没有声明构造方法,该程序能正确执行吗?为什么?
18. 在调用子类构造方法之前会先调用父类没有参数 的构造方法,其目的是?
五、关于两个常见注解 @Override & @Deprecated
一、== & equals
1. == 和 equals 的区别是什么?(说法一)
== : 它的作用是判断两个对象的地址是不是相等。
即,判断两个对象是不是同 一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
情况 1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个 对象时,等价于通过“==”比较这两个对象。
情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来 两个对象的内容相等;
若它们的内容相等,则返回 true (即,认为这两 个对象相等)。
举个例子:
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 中的 equals 方法是被重写过的,因为 object 的 equals 方法是 比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存 在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没 有就在常量池中重新创建一个 String 对象。
2. == 和 equals 的区别是什么?(说法二)
java中其实只有 == 比较,没有equals比较,其实底层还是 ==。
只不过equals是很多类定义个方法而已,而这个方法把内存地址的比较改成基础数据类型的比较。
就算是String ,Integer,Long 等都回归到基础数据类型的比较。
java中的数据类型,可分为两类:基本数据类型和引用数据类型
基本数据类型,也称原始数据类型
byte,short,char,int,long,float,double,boolean 他们之间的比较,应用双等号(==),比较的是他们的
值。
- == 在基础数据类型,就是字面值的比较,和数据类型无关,只要相同就是true
- == 在封装数据类型中,它内存地址比较。在java中Short,Integer,Long都Byte范围的长度进来缓存,只要在-128-127都是相等的,其他的都是内存地址比较都是false
3. == 和 equals的区别(说法三)
- 在java中理论上是没有eqauls比较的概念。本身只有==内存地址比较
- eqauls是一个方法,来自Object类,如果一个类没有重写eqauls方法,默认就是内存地址比较
-
- equals
- Person p1 = new Person(1,”飞哥”)
- Person p2 = new Person(1,”飞哥”)
- p1==p2 === false
- p1.eqauls(p2) === true===
- set —- >HashMap
- 基础数据数据类型,==就是字面值比较
理论来判断两个对象是否是相等,值要满足eqauls是true是行。和hashcode没任何关系。
在开发中,如果把两个相同值的对象放到set或者hashmap结构中,要到过滤和去重就必须满足两个条件。
hashcode要一致,同时equals方法返回true.
因为set的底层hashMap,hashmap的key如何相同必须保证:hashcode要一致,
同时equals方法返回true
-
- float a= 10;int b = 10; a==b true
4. equals方式是来自Object方法
- 如果我们定义一个类,如果没有覆盖equals方法的话,全部都是内存地址比较。
Parent parent1 = new parent(1,2);
Parent parent2 = new parent(1,2);
parent1.equals(parent2);==相当于 parent1==parent2 == false
- 因为 String,Integer ,Long、Double等是它们都重写了Object的equals方法,把equals方法中的内存地址比较改成了基础数据类型的字面值比较。根据基础数据类型的比较规则,只要字面值相同就是true。
5. 引用类型(类、接口、数组)
public class testDay {
public static void main(String[] args) {
String s1 = new String("11");
String s2 = new String("11");
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
}
结果是:
false true
s1和s2都分别存储的是相应对象的地址。所以如果用 s1== s2时,比较的是两个对象的地址值(即比较
引用是否相同),为false。
而调用equals方向的时候比较的是相应地址里面的值,所以值为true。这里就需要详细描述下equals()
了。
6. equals()方法详解
equals()方法是用来判断其他的对象是否和该对象相等。
其再Object里面就有定义,所以任何一个对象都有equals()方法。区别在于是否重写了该方法。
先看下源码:
public boolean equals(Object obj) {
return (this == obj);
}
很明显Object定义的是对两个对象的地址值进行的比较(即比较引用是否相同)。
但是为什么String里面调用equals()却是比较的不是地址而是堆内存地址里面的值呢。
这里就是个重点了,像String 、Math、Integer、Double等这些封装类在使用equals()方法时,已经覆盖了object类的equals()方法。
看下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;
}
重写了之后就是这是进行的内容比较,而已经不再是之前地址的比较。
依次类推Math、Integer、Double等这些类都是重写了equals()方法的,从而都会回归到基础数据类型的字面值比较。
需要注意的是当equals()方法被override时,hashCode()也要被override。
按照一般hashCode()方法的实现来说,相等的对象,它们的hashcode一定相等。
为什么会这样,这里又要简单提一下hashcode了。
7. 两个对象的 hashCode() 相同, 那么 equals() 也一定为 true 吗?
不对,两个对象的 hashCode() 相同,equals() 不一定 true。
代码示例:
String str1 = "keep";
String str2 = "brother";
System. out. println(String. format("str1:%d | str2:%d", str1. hashCode(),str2. hashCode()));
System. out. println(str1. equals(str2));
执行的结果:
str1:1179395 | str2:1179395
false
代码解读:
很显然“keep”和“brother”的 hashCode() 相同,然而 equals() 则为 false,
因为在散列表中,hashCode() 相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值
对相等。
8. hashcode()浅谈
Hashcode覆盖的作用其实:未来这个对象要添加到java中数据结构比如:链表,map中,这个时候提供
的数字。
比如明明是java中==和equals和hashCode的区别问题,怎么一下子又扯到hashcode()上面去了。
你肯定很郁闷,好了,我打个简单的例子你就知道为什么==或者equals的时候会涉及到hashCode。
举例说明下:
如果你想查找一个集合中是否包含某个对象,那么程序应该怎么写呢?
不要用indexOf方法的话,就是从集合中去遍历然后比较是否想到。万一集合中有10000个元素呢,累屎
了是吧。
所以为了提高效率,哈希算法也就产生了。核心思想就是将集合分成若干个存储区域(可以看成一个个
桶),每个对象可以计算出一个哈
希码,可以根据哈希码分组,每组分别对应某个存储区域,这样一个对象根据它的哈希码就可以分到不
同的存储区域(不同的区域)。
所以再比较元素的时候,实际上是先比较hashcode,如果相等了之后才去比较equal方法。
看下hashcode图解:
一个对象一般有key和value,可以根据key来计算它的hashCode值,再根据其hashCode值存储在不同
的存储区域中,如上图。
不同区域能存储多个值是因为会涉及到hash冲突的问题。简单如果两个不同对象的hashCode相同,这
种现象称为hash冲突。
简单来说就是hashCode相同但是equals不同的值。对于比较10000个元素就不需要遍历整个集合了,只
需要计算要查找对象的key的
hashCode,然后找到该hashCode对应的存储区域查找就over了。
所以判断相等的流程如图所示:
大概可以知道,先通过hashcode来比较,如果hashcode相等,那么就用equals方法来比较两个对象是否相等。
再重写了equals最好把hashCode也重写。其实这是一条规范,如果不这样做程序也可以执行,只不过会隐藏bug。
一般一个类的对象如果会存储在HashTable,HashSet,HashMap等散列存储结构中,那么重写equals后最好也重写hashCode。
总的来说:
- hashCode是为了提高在散列结构存储中查找的效率,在线性表中没有作用。
- equals重写的时候hashCode也跟着重写
- 两对象equals如果相等那么hashCode也一定相等,反之不一定。
9. 为什么重写 equals时必须重写 hashCode 方法?
面试官可能会问你:
“你重写过 hashcode 和 equals 么,为什么重写 equals时必须重写 hashCode 方法?”
9.1. hashCode() 介绍
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整 数。
这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode() 定义 在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函
数。
散列表存储的是键值对(key-value),
它的特点是:能根据“键”快速的检索出对应 的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
9.2. 为什么要有hashCode
我们以“HashSet 如何检查重复为例子来说明为什么要有 hashCode
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断 对象加入的位置,同时也会与其他已经加入的对象的
hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。
但是如果发现有 相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相 等的对象是否真的相同。
如果两者相同,HashSet 就不会让其加入操作成功。 如果不同的话,就会重新散列到其他位置。
这样我们就大大减少了 equals 的次数,相应就大大提高 了执行速度。
9.3. hashCode()与equals()的相关规定
- 如果两个对象相等,则 hashcode 一定也是相同的
- 两个对象相等,对两个对象分别调用 equals 方法都返回 true
- 两个对象有相同的 hashcode 值,它们也不一定是相等的
- 因此,equals方法被覆盖过,则hashCode 方法也必须被覆盖
- hashCode() 的默认行为是对堆上的对象产生独特值。
如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个 对象指向相同的
数据)
10. java里equals和hashCode之间什么关系
推荐查看文章:https://www.cnblogs.com/tanshaoshenghao/p/10915055.html
10.1 两者的关系?
两者没有关系,
10.2 equals
- 是判断两个对象是否相同的方法。默认所有的类,如果没有覆盖都是调用Object的eqauls方法。也是一个 ==比较
package com.kuangstudy.equals;
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
package com.kuangstudy.equals;
public class UserTest {
public static void main(String[] args) {
User user1 = new User("yykk",1);
User user2 = new User("yykk",2);
System.out.println(user1.equals(user2));
}
}
核心代码分析
System.out.println(user1.equals(user2));
得到结果确实是:false
为什么是false 呢?
- 1:eqauls方法从哪里来的?从父类而来,如果一个类没有继承任何类,它的父类就是Object
- 2:因为Object里的equals方法是内存地址比较
public boolean equals(Object obj) {
return (this == obj);
}
- 所以这个代码的话,就等价于
System.out.println(user1==user2);
- 所以答案是false.
如果我想让属性值相同的对象,代表是同一个对象的话?
答案:重写Object的eqauls,重写指定规则。
System.out.println(user1.equals(user2));
@Override
public boolean equals(Object user2) {
if (user1 == user2) return true;
if (user2 == null || getClass() != o.getClass()) return false;
User user = (User) user2;
return user1.age == user.age && Objects.equals(user1.name, user.name);
}
什么样子的情况下要覆盖呢?
- 要过滤,比如一个集合中有很多对象,这个对象很多值相同,我想过滤掉,可以考虑覆盖eqauls方法
- Set功能,不允许重复无序,底层是:HashMap
方法具体介绍
如果要比较实际内存中的内容,那就要用equals方法,但是!!!
如果是你自己定义的一个类,比较自定义类用equals和==是一样的,都是比较句柄地址,
因为自定义的类是继承于object,而object中的equals就是用==来实现的,你可以看源码。
那为什么我们用的String等等类型equals是比较实际内容呢,是因为String等常用类已经重写了object中的equals方法,
让equals来比较实际内容。
在一般的应用中你不需要了解hashcode的用法,但当你用到hashmap,hashset等集合类时要注意下hashcode。
两个对象的 hashCode() 相同, 那么 equals() 也一定为 true吗?
不对,两个对象的 hashCode() 相同,equals() 不一定 true。
代码示例:
String str1 = "keep";
String str2 = "brother";
System. out. println(String. format("str1:%d | str2:%d", str1. hashCode(),str2. hashCode()));
System. out. println(str1. equals(str2));
执行的结果:
str1:1179395 | str2:1179395
false
代码解读:
很显然“keep”和“brother”的 hashCode() 相同,然而 equals() 则为 false,
因为在散列表中,hashCode() 相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。
二、API 相关
1. Java 中的 Math. round(-1. 5) 等于多少?
Math.round(-1.5)的返回值是-1。四舍五入的原理是在参数上加0.5然后做向下取整。
我们可以通过大量实验看下结果
public class test {
public static void main(String[] args){
System.out.println(Math.round(1.3)); //1
System.out.println(Math.round(1.4)); //1
System.out.println(Math.round(1.5)); //2
System.out.println(Math.round(1.6)); //2
System.out.println(Math.round(1.7)); //2
System.out.println(Math.round(-1.3)); //-1
System.out.println(Math.round(-1.4)); //-1
System.out.println(Math.round(-1.5)); //-1 --------这里要注意
System.out.println(Math.round(-1.6)); //-2
System.out.println(Math.round(-1.7)); //-2
}
}
三、字符/字符串相关
1. 字符型常量和字符串常量的区别
形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的 若干个字符
含义上: 字符常量相当于一个整形值( ASCII 值),可以参加表达式运算 字 符串常量代表一个地址值(该字符串在内存中存放位置)
占内存大小: 字符常量只占 2 个字节 字符串常量占若干个字节(至少一个 字符结束标志) (注意: char 在Java中占两个字节)
2. String 属于基础的数据类型吗?
String是final修饰的java类,java中的基本类型一共有8个,它们分别为:
- 字符类型:byte,char
- 基本整型:short,int,long
- 浮点型:float,double
- 布尔类型:boolean
此外需要说明 有的文章中吧void也算是一种基本的数据类型
3. Java 中操作字符串都有哪些类?它们之间有什么区别?
操作字符串的类有:String、StringBuffer、StringBuilder。
String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成
新的 String 对象,然后将指针指向
新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变
字符串内容的情况下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线
程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用
StringBuilder,多线程环境下推荐使用 StringBuffer。
4. String str="i"与 String str=new String("i")一样吗?
不一样,因为内存的分配方式不一样。
String str="i"的方式,Java 虚拟机会将其分配到常量池中;而 String str=new String("i") 则会被分到
堆内存中。
代码示例:
String x = "叶痕秋";
String y = "叶痕秋";
String z = new String("叶痕秋");
System.out.println(x == y); // true
System.out.println(x == z); // false
String x = "叶痕秋" 的方式,Java 虚拟机会将其分配到常量池中,而常量池中没有重复的元素,比如当
执行“叶痕秋”时,java虚拟机会先在常量池中检索是否已经有“叶痕秋”,如果有那么就将“叶痕
秋”的地址赋给变量,如果没有就创建一个,然后在赋给变量;而 String z = new String(“叶痕秋”)
则会被分到堆内存中,即使内容一样还是会创建新的对象。
5. String & StringBuffer & StringBuilder
String StringBuffer 和 StringBuilder 的区别 是什么 String 为什么是不可变的
5.1. 可变性
简单的来说:String 类中使用 final 关键字字符数组保存字符串,
private final char value[],所以 String 对象是不可变的。
而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,
在 AbstractStringBuilder 中 也是使用字符数组保存字符串 char[] value 但是没有用 final 关键字修饰,
所以 这两种对象都是可变的。
StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,
大家可以自行查阅源码。
AbstractStringBuilder.java
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
5.2. 线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。
AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了 一些字符串的基本操作,
如 expandCapacity、append、insert、indexOf 等公共方法。
StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以 是线程安全的。
StringBuilder 并没有对方法进行加同步锁,所以是非线程安全 的。
5.3. 性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将 指针指向新的 String 对象。
StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。
相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
5.4. 对于三者使用的总结
① 操作少量的数据 = String
② 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
③ 多线程操作字符串缓冲区下操作大量数据 = StringBuffer
6. String、StringBuffer、StringBuilder的区别
三者共同之处:都是final类,不允许被继承,主要是从性能和安全性上考虑的,因为这几个类都是经常被
使用着,且考虑到防止其中的参
数被参数修改影响到其他的应用。
- StringBuffer是线程安全,可以不需要额外的同步用于多线程中;
- StringBuilder是非同步,运行于多线程中就需要使用着单独同步处理,但是速度就比StringBuffer快多了;
- StringBuffer与StringBuilder两者共同之处:可以通过append、indert进行字符串的操作。
- String实现了三个接口:Serializable、Comparable、CarSequence
- StringBuilder只实现了两个接口Serializable、CharSequence,相比之下String的实例可以通过compareTo方法进行比较,其他两个不可以。
这三个类之间的区别主要是在两个方面,即运行速度和线程安全这两方面。
7. 关于Serializable的问题?
后续的开发中SpringCloud或者Dubbo的远程通讯的时候,实体需要去实现一个所谓的Serializable接
口。
- RestTemplate
- RPC
- Redis
- MQ
String name = "飞哥是一个帅哥";
A服务 resttemplate.get("/xxxxx","xxxxx");--------GET----> B服务
User user = new User();// implments Serializable -- Socket
A服务 resttemplate.get("/xxxxx",user);--------GET----> B服务
8. String问题和弊端?
就因为String存在问题和弊端所以才有了:StringBuffer、StringBuilder。
String sql = "select * from table";
sql += "where a = 1 and b=1";
sql += " and c = 1 and d=1";
StringBuffer stringbuffer = new StringBuffer();
stringbuffer.append("select * from table");
stringbuffer.append("where a = 1 and b=1");
stringbuffer.append("and c = 1 and d=1");
9. StringBuffer是线程安全,StringBuilder是非线程安全的。
- 在我们实际应用开发中,我大部环境都是多线程的并发请求方式。也就是说98%都==考虑线程安全==的问题,2%的业务可能不需要考虑到线程安全。
- 和硬件有关,CPU多核时代,为了不浪费CPU,大部分语言都为迎合硬件都提供多线程。多线程的处理可以提升网站的并发执行能力,如果没有多线程,是单线程的话,如果我请求百度,你就必须等我执行完毕,才能访问。如果这个1000W用户量访问,就会影响效率。
10. 那么Srring问题和弊端是什么?
10.1. 首先说运行速度,或者说是执行速度
在这方面运行速度快慢为:StringBuilder > StringBuffer > String
String最慢的原因:String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对
象一旦创建之后该对象
是不可更改的,但后两者的对象是变量,是可以更改的。以下面一段代码为例:
String str="abc";
System.out.println(str);
str=str+"de";
System.out.println(str);
运行这段代码会发现先输出“abc”,然后又输出“abcde”,好像是str这个对象被更改了,其实,这只
是一种假象罢了,JVM对于这几行代码是这样处理的,首先创建一个String对象str,并把“abc”赋值给
str,然后在第三行中,其实JVM又创建了一个新的对象也名为str,然后再把原来的str的值和“de”加
起来再赋值给新的str,而原来的str就会被JVM的垃圾回收机制(GC)给回收掉了,所以,str实际上并
没有被更改,也就是前面说的String对象一旦创建之后就不可更改了。
所以,Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,
所以执行速度很慢。
而StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创
建和回收的操作,所以速度要比String快很多。
另外,有时候我们会这样对字符串进行赋值
String str="abc"+"de";
StringBuilder stringBuilder=new StringBuilder().append("abc").append("de");
System.out.println(str);
System.out.println(stringBuilder.toString());
这样输出结果也是“abcde”和“abcde”,但是String的速度却比StringBuilder的反应速度要快很多,
这是因为第1行中的操作和String str="abcde";是完全一样的,所以会很快,而如果写成下面这种形式
public static void main(String[] args) {
long a=new Date().getTime();
String cc="";
int n=10000;
for (int i = 0; i < n; i++) {
cc+="."+i;
}
System.out.println("String使用的时间"+(System.currentTimeMillis()-a)/1000.0+"s");
long s1=System.currentTimeMillis();
StringBuilder sb=new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append("."+i);
}
System.out.println("StringBuilder使用的时间"+(System.currentTimeMillis()-s1)/1000.0+"s");
long s2=System.currentTimeMillis();
StringBuffer sbf=new StringBuffer();
for (int i = 0; i < n; i++) {
sbf.append("."+i);
}
System.out.println("StringBuffer使用的时间"+(System.currentTimeMillis()-s2)/1000.0+"s");
}
10.2 再来说线程安全
在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的
如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有
synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能
保证线程安全,有可能会出现一些错误的操作。
所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用
速度比较快的StringBuilder。
(一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞)
10.3 总结
String:适用于少量的字符串操作的情况
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
11. String a = "123"; String b = "123"; a==b 吗?为什么??
学过编程语言的应该没人不会做吧,答案是 true 。
很多人都觉得这个问题是不是太简单了,其实不然。这里面包含的内存,String存储方式等知识点。
现在我以我的理解来分析、解释一下这个问题的底层原因。如有不足,还请大家留言指出。
String a = "123";
String b = "123";
System.out.println(a == b);
答案:true
学习Java的人都知道,JVM的重要性。在JVM中有一块区域叫做常量池,具体虚拟机的结构可以参考一篇博客
(https://blog.csdn.net/qq_40722827/article/details/103236038)。
常量池中的数据是那些在编译期间被确定的,并被保存在已编译的.class文件中的一些数据。
我们定义的String a = "123"; String b = "123"; 这些语句,我们拆分开来看:
编译器:指类中成员变量
- 123,等号右边的指的是==编译期==间可以被确定的内容,都维护在常量池中。
- str ,等号左边的指的是一个引用,引用的内容是等号右边数据在常量池中的地址
- String 这是引用类型
栈有一个特点,就是数据共享。回到最初的问题,String a = "123",编译的时候,在常量池中创建了一
个常量"123",然后String b= "123",先去常量池中找有没有这个"123",发现常量池中有这
个“123”,然后b也指向常量池中的"123",所以a==b返回的是true,因为a和b指向的都是常量池中
的"123"这个字符串的地址。
其实其他基本数据类型也都是一样的:先看常量池中有没有要创建的数据,有就返回数据的地址,没有
就创建一个。
看完上面这个,我们再来看一下下面的这个:
String a = new String("234");
String b = new String("234");
System.out.println(a == b);
答案:false
那么,这儿的答案为什么是 false 呢???
原因:Java虚拟机的解释器每遇到一个new关键字,都会在堆内存中开辟一块内存来存放一个String对
象,所以a,b指向的堆内存虽然存储的都是“234”,但是由于两块不同的堆内存,因此 a==b 返回的仍
然是false。
12. String在内存中如何存储
JDK1.8中JVM把String常量池移入了堆中,同时取消了“永久代”,改用元空间代替(Metaspace)
java中对String对象特殊对待,所以在heap区域分成了两块,一块是字符串常量池(String constant pool),
用于存储java字符串常量对象,另一块用于存储普通对象及字符串对象。
string的创建有两种方法:
public static void main(String[] args) {
String a = "abc"; //第一种
String b= new String("abc"); //第二种
String c = "abc";
System.out.println(a == b);//false
System.out.println(a == c);//true
}
对于第一种,此创建方法会在String constant pool中创建对象。jvm会首先在String constant pool 中
寻找是否已经存在"abc"常量,如果没有则创建该常量,并且将此常量的引用返回给String a;
如果已有"abc" 常量,则直接返回String constant pool 中“abc” 的引用给String a。
对于第二种,jvm会直接在非String constant pool 中创建字符串对象,然后把该对象引用返回给String
b,并且不会把"abc” 加入到String constant pool中。new就是在堆中创建一个新的String对象,不
管"abc"在内存中是否存在,都会在堆中开辟新空间。
虽然new String()方法并不会把"abc” 加入到String constant pool中,但是可以手动调用
String.intern(),将new 出来的字符串对象加入到String constant pool中。
String s1 = new String("abc");
String s2 = "abc";
System.out.println(s1 == s2); //false
System.out.println(s1.intern() == s2); //true
当一个String实例调用intern()方法时,会查找常量池中是否有相同的字符串常量,
如果有,则返回其的引用,如果没有,则在常量池中增加一个等于str的字符串并返回它的引用,
由于s2已经在常量池中,所以s1.intern()不会再创建,而是直接引用同一个"aaa"。
public static void main(String[] args) {
String s1 = "abc";//字符串常量池
String s2 = "xyz";//字符串常量池
String s3 = "123";//字符串常量池
String s4 = "A";//字符串常量池
String s5 = new String("abc"); //堆里
char[] c = {'J','A','V','A'};
String s6 = new String(c);//堆里
String s7 = new String(new StringBuffer());//堆里
}
字符串在内存中的存储情况如下图所示:
13. 总结
对于字符串:其对象的引用都是存储在栈中的,
如果是【编译期已经创建好(直接用双引号定义的)的就存储在常量池中】,
如果是【运行期(new出来的)才能确定的就存储在堆中】。
对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。
四、面向对象相关
1. 抽象类必须要有抽象方法吗?
不需要,抽象类不一定非要有抽象方法。
示例代码:
abstract class Cat {
public static void sayHi() {
System. out. println("hi~");
}
}
上面代码,抽象类并没有抽象方法但完全可以正常运行。
2. 普通类和抽象类有哪些区别?
- 普通类不能包含抽象方法,抽象类可以包含抽象方法。
- 抽象类不能直接实例化,普通类可以直接实例化。
3. 抽象类能使用 final 修饰吗?
不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类,
4. 接口和抽象类有什么区别?
- 实现:抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
- 构造函数:抽象类可以有构造函数;接口不能有。
- 实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
- 访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。
5. 静态变量和实例变量的区别?
静态变量存储在方法区,属于类所有.
实例变量存储在堆当中,其引用存在当前线程栈.需要注意的是从JDK1.8开始用于实现方法区的PermSpace被MetaSpace取代了.
6. Object中有哪些公共方法?
equals(),clone(),getClass(),notify(),notifyAll(),wait(),toString
7. java 创建对象的几种方式
java中提供了以下四种创建对象的方式:
- new创建新对象
- 通过反射机制
- 采用clone机制
- 通过序列化机制
8. 在一个静态方法内调用一个非静态成员为什么是非法的
由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非 静态变量,也不可以访问
非静态变量成员。
9. 在 Java 中定义一个不做事且没有参数的构造 方法的作用
Java 程序在执行子类的构造方法之前,如果没有用 super() 来调用父类特定 的构造方法,则会调用父类
中“没有参数的构造方法”。
因此,如果父类中只定 义了有参数的构造方法,而在子类的构造方法中又没有用 super() 来调用父类 中
特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没 有参数的构造方法可供执行。
解决办法是在父类里加上一个不做事且没有参数 的构造方法。
10. 接口和抽象类的区别是什么
- 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始 接口方法可以有默认实现),抽象类可以有非抽象的方法
- 接口中的实例变量默认是 final 类型的,而抽象类中则不一定
- 一个类可以实现多个接口,但最多只能实现一个抽象类
- 一个类实现接口的话要实现接口的所有方法,而抽象类不一定
- 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口 的对象
从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是 行为的抽象,是一种行为的规范。
11. 成员变量与局部变量的区别有那些
- 从语法形式上,看成员变量是属于类的,而局部变量是在方法中定义的 变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所 修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员 变量和局部变量都能被 final 所修饰;
- 从变量在内存中的存储方式来看,成员变量是对象的一部分,而对象存 在于堆内存,局部变量存在于栈内存
- 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对 象的创建而存在,而局部变量随着方法的调用而自动消失。
- 成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(一种情 况例外被 final 修饰的成员变量也必须显示地赋值);而局部变量则不会自动赋值。
12. 创建一个对象用什么运算符? 对象实体与对象引用有何不同
new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象 实例
(对象引用存放在栈内存中)。
一个对象引用可以指向 0 个或 1 个对象 (一根绳子可以不系气球,也可以系一个气球);
一个对象可以有 n 个引用指向 它(可以用 n 条绳子系住一个气球)。
13. 什么是方法的返回值?返回值在类的方法里的作用是什么
方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!
(前 提是该方法可能产生结果)。
返回值的作用:接收出结果,使得它可以用于其他 的操作!
14. 一个类的构造方法的作用是什么?若一个类没有声明构造方法,该程序能正确执行吗?为什么?
主要作用是完成对类对象的初始化工作。可以执行。
因为一个类即使没有声明 构造方法也会有默认的不带参数的构造方法。
15. 构造方法有哪些特性
- 名字与类名相同;
- 没有返回值,但不能用 void 声明构造函数;
- 生成类的对象时自动执行,无需调用。
16. 静态方法和实例方法有何不同
- 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对 象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用 静态方法可以无需创建对象。
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量 和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无 此限制.
17. 对象的相等与指向他们的引用相等,两者有什么 不同?
对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们 指向的内存地址是否相等。
18. 在调用子类构造方法之前会先调用父类没有参数 的构造方法,其目的是?
帮助子类做初始化工作。
19. Java中,接口可以包含哪些类型的成员?
在Java中,接口除了可以包含抽象方法外,还可以包含其他类型的成员。以下是Java接口中可以包含的
成员类型:
常量:接口可以定义常量,即静态的、不可修改的属性。
常量在接口中默认为 public、static 和 final,可以通过接口名直接访问。
例如:
默认方法:从Java 8开始,接口可以包含默认方法(Default Method),即具有默认实现的方法。
默认方法使用 default 关键字进行标记,可以在接口中提供方法的默认实现。
默认方法允许在接口中添加新的方法,而不会破坏已有的实现类。
例如:
静态方法:从Java 8开始,接口也可以包含静态方法(Static Method),即在接口中定义的静态方法。
静态方法使用 static 关键字进行标记,可以通过接口名直接调用。
例如:
需要注意的是,接口中的成员默认是公共的(public),因此可以在接口外部访问。此外,接口中的成
员不允许使用访问修饰符 private 或 protected。
通过定义常量、默认方法和静态方法,接口提供了更多的灵活性和功能扩展性,使得接口成为一种强大
的工具,用于定义类之间的契约和共享行为。
五、关于两个常见注解 @Override & @Deprecated
注解:是一种面向对象的注释
问:你为什么要写注释
- 起到一个标记和标注的作用
- 方便我们自己后续或者别人查看你代码的时候能够读得懂
// name="zhengge" limit=10 timout=1
@Limiter(name="zhengge",limit=10,timout=1 )
public void unlock() {
lock.unlock();
}
问:为什么要要去用注解呢?
- 就是因为:我们可以通过反射获取这个类,获取方法,获取属性,获取包名、获取参数。它们都可以通过反射获取到注解信息。
- 也同时告诉你一个道理:注解可以定义在这些类,方法、属性、包名、参数中。
问:注解给谁看呢?怎么获取呢?
- 注解处理和注释都起到一个标记的作用,但是注释没办法获取,但是注解可以通过获取类,方法,属性,参数,包名进行获取
- 一句话:给反射看,用反射来获取。
问:场景
- 架构中spring框架,获取你自己去进行拦截通用处理的时候
- 日志拦截需要明确告诉方法执行的逻辑是什么? @Log + LogAspect(这个类中肯定通过类,方法、属性、包名、参数任意一种方式获取注解信息,进行业务员逻辑处理。)
- 权限控制 @CheckLogin + AuthAspect(这个类中肯定通过类,方法、属性、包名、参数任意一种方式获取注解信息,进行业务员逻辑处理。)
- 限流控制等 @Limiter + LimterAspect(这个类中肯定通过类,方法、属性、包名、参数任意一种方式获取注解信息,进行业务员逻辑处理。)
// 保存用户方法
@Log(desc="保存用户方法")
public void saveuser() {
}
@Log(desc="修改用户方法")
public void 修改user() {
}
@Override
它是所以子类继承父类的抽象方法的一种标记。
在子类的重写方法上写上@Override 告诉子类这个方法是重写父类的方法,这样更新明确。
- 当然你也可以不写,但是建议写上去,因为往往在开发中,子类的方法很多,重写的方法很多,所以为了区分建议大家写上@Override
@Deprecated
- 如果@Deprecated类,代表这个类,即将废弃和使用
- 如果@Deprecated方法,代表这个方法,即将废弃和使用
- 如果@Deprecated属性,代表这个属性,即将废弃和使用
总结
- 如果未来在开发中,如果那你类即将要改写,要重构,或者有新的替代的解决方案,你可以把旧的类,方法,属性增加@Deprecated告诉的开发者,这个可能未来有新的替代解决方案,告诉可以未来关注新的解决方案,
- 如果被标记@Deprecated的类,方法,属性还是可以使用。建议开发者去下载最新的替代解决方案。把替换掉。
六、关键字
1. final
1.1 作用
相信对于final的用法,大多数人都可以随口说出三句话:
1、被final修饰的类不可以被继承
2、被final修饰的方法不可以被重写
3、被final修饰的变量不可以被改变
重点就是第三句。被final修饰的变量不可以被改变,什么不可以被改变呢,是变量的引用?还是变量里面的内容?
还是两者都不可以被改变?写个例子看一下就知道了:
1.2 Java 为什么要加 final 关键字了!
我正在读一本有关Java的书,书上说可以声明整个类为final。我能想到的任何地方,都不会使用这个。
对于编程,我只是一个新手,我想知道程序员是否实际在他们的程序中这样做。如果他们这样做,他们什么时候使用它,这样我能更好地
了解它,知道什么时候使用它。如果Java是面向对象的,并且你声明一个final类,它是否不再认为类拥有这个对象的特征?
回答:
- final类只是一个不能扩展的类。如果你不想让其它人再次扩展你的类,就可以将其声明为final。如果Java是面向对象的,并且你声明一个final类,它是否不再认为类拥有这个对象的特征?在某种意义上是的。通过将类标记为final,您将禁用该部分代码的强大而灵活的语言特性。然而,一些类不应该(在某些情况下不能)被设计为以良好的方式考虑子类。在这些情况下,将类标记为final是有意义的,即使它限制了OOP。(但是请记住,final类仍然可以扩展为另一个非final类)。
1.3 什么样子的情况下一个类需要被Final进行修饰呢?
- 说白一个类如果你觉得这个类的已经很完美了。我不需要任何人进行修改和扩展的时候,你可以考虑设置final
- jdk中就存在大量的final修饰的比如:String、Integer、Long、Float、枚举类。
- 作用1:起到一个保护和安全的一个作用
- 作用2:对内修改开放 ,对外修改关闭。(String,对内修改开放(oracle的团队),对于程序员来说就是关闭的,)
- 一句话:被final修饰的类就是只读的类。
1.4 final关键字的好处
下面总结了一些使用final关键字的好处
- final关键字提高了性能。JVM和Java应用都会缓存final变量。
- final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
- 使用final关键字,JVM会对方法、变量及类进行优化。
1.5 不可变类
创建不可变类要使用final关键字。不可变类是指它的对象一旦被创建了就不能被更改了。String是不可变类的代表。
不可变类有很多好处,譬如它们的对象是只读的,可以在多线程环境下安全的共享,不用额外的同步开销等等。
- final关键字可以用于成员变量、本地变量、方法以及类。
- final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。
- 你不能够对final变量再次赋值。
- 本地变量必须在声明时赋值。
- 在匿名类中所有变量都必须是final变量。
- final方法不能被重写。
- final类不能被继承。
- final关键字不同于finally关键字,后者用于异常处理。final关键字容易与finalize()方法搞混,后者是在Object类中定义的方法,是在垃圾回收之前被JVM调用的方法。
- 接口中声明的所有变量本身是final的。
- final和abstract这两个关键字是反相关的,final类就不可能是abstract的。
- final方法在编译阶段绑定,称为静态绑定(static binding)。
- 没有在声明时初始化final变量的称为空白final变量(blank final variable),它们必须在构造器中初始化,或者调用this()初始化。不这么做的话,编译器会报错“final变量(变量名)需要进行初始化”。
- 将类、方法、变量声明为final能够提高性能,这样JVM就有机会进行估计,然后优化。
- 按照Java代码惯例,final变量就是常量,而且通常常量名要大写
七、枚举关于单例的问题?
1. 关于枚举
- 定义是enum来修改类,你明确一个道理,枚举是一个类.
- 它的构造函数是私有的private,说明外部不能直接去实例化,只能通过自己定义签名进行实例化。
- enum类不可以继承类,不继承抽象类,但是可以实现接口。因为enum是一个特殊类(final的类)
2. 枚举关于单例模式的问题?
互联网开发中,很多面对设计模式的问题,枚举可以单例设计模式,问:
3. 枚举真的是单例的吗?
答案:枚举本身不是单例的。
package com.zheng.demo03;
public enum SingleEnum {
MALE("男"),WOMAN("女");
private String name;
SingleEnum(String name){
this.name = name;
}
}
4. 单例对象是指是什么?
- 在内存空间中只有一份内存地址。
- 恶汉,懒汉,双重check机制,枚举机制等等
- 首先第一件事情:
-
- 造函数私有化
- 通过成员方法暴露当前对象初始化的工作。
5. 怎么证明枚举不是单例的呢?
不是,每个枚举引用都是独立的对象内存空间。
6. 问枚举在什么情况下是单例的呢?
但是枚举在一种情况是下单例,就是枚举中定义单个对象的情况下就是单例的。
7. spring的bean是单例的吗?
spring的bean是通过所谓恶汉,懒汉,枚举,抽象类或者双从检查机制实现的单例设计模式吗?
记住一句话:
单例设计模式:它的思想就是告诉你一个对象在内存空间一个只有一个内存地址。
实现的方式1:所谓的恶汉,懒汉,枚举,抽象类、双从检查机制等能够实现单例对象的创建以外?
springbean的实现单例对象的机制:注册表(map)
答案:不是,它只是一个单例的设计思想,它的实现就是注册列表的机制,其实就是一个Map
public class ApplicationClass{
// 成员变量
private static Map<BeanDefintion> map = new ConcurrentHashMap<>();
map.put("com.zheng.service.UserServiceImpl",new UserServiceImpl());