==和equals比较
==:对比的是栈的值,由于不通类型数据存放位置不同,基本数据类型对比的是变量值,对比引用类型时,对比的其实是堆内存的地址
equals:Object中默认的同样是 ==比较,那为什么我们平时使用string类的成员方法equals,对比的是值呢,其实是很多类中重写了Object类中的equals。以String类为例,我们从源码可以看到重写的方法,本质也是对比字符串哪一个字符
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;
}
什么是匿名内部类
匿名内部类可以使你的代码更加简洁,你可以在定义一个类的同时对其进行实例化。它与局部类很相似,不同的是它没有类名,如果某一个类你只需要使用一次,就可以使用匿名内部类
public class HelloWorldAnonymousClasses {
/**
* 包含两个方法的HelloWorld接口
*/
interface HelloWorld {
public void greet();
public void greetSomeone(String someone);
}
public void sayHello() {
// 1、局部类EnglishGreeting实现了HelloWorld接口
class EnglishGreeting implements HelloWorld {
String name = "world";
public void greet() {
greetSomeone("world");
}
public void greetSomeone(String someone) {
name = someone;
System.out.println("Hello " + name);
}
}
HelloWorld englishGreeting = new EnglishGreeting();
// 2、匿名类实现HelloWorld接口
HelloWorld frenchGreeting = new HelloWorld() {
String name = "tout le monde";
public void greet() {
greetSomeone("tout le monde");
}
public void greetSomeone(String someone) {
name = someone;
System.out.println("Salut " + name);
}
};
// 3、匿名类实现HelloWorld接口
HelloWorld spanishGreeting = new HelloWorld() {
String name = "mundo";
public void greet() {
greetSomeone("mundo");
}
public void greetSomeone(String someone) {
name = someone;
System.out.println("Hola, " + name);
}
};
englishGreeting.greet();
frenchGreeting.greetSomeone("Fred");
spanishGreeting.greet();
}
public static void main(String... args) {
HelloWorldAnonymousClasses myApp = new HelloWorldAnonymousClasses();
myApp.sayHello();
}
}
string +和append什么区别
使用“+”运算符和使用StringBuilder类的append方法的区别在于它们对字符串的处理方式不同
1、当你使用“+”运算符连接两个字符串时,本质是调用stringbuilder 方法append,每次都返回一个新的字符串对象。
public static void main(String[] args) {
String str1 = "hello";
String str2 = str1 + " coisini";
System.out.println(str2);
}
javap -c
public class com.lujichao.learn.algorithm.Stringadd {
public com.lujichao.learn.algorithm.Stringadd();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String hello
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String coisini
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;调用append 方法
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
23: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload_2
27: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: return
}
2、StringBuilder类的append方法则是将指定的字符串附加到原始字符串中,而不会返回新的字符串对象。
在效率方面,如果你只是连接两个小字符串,例如string s = “a” + “b”;那么使用“+”运算符可能会更。然而,当你需要在循环中连接多个字符串时使用+时每次都要new stringbuiled,较为耗时,使用StringBuilder类会更高效。
final关键词的作用?
最终的,可以修饰类,方法和变量
1、修饰类时,当前类不能被继承,大家可以看下我们常用的String、Integer类,这些不能被继承的类,均有final关键词修饰
2、修饰方法时,方法不能被覆盖,但是可以重载
3、修饰基本数据类型变量时,数值一旦被初始化后便不能修改;修饰引用类型变量时,在对其实例化之后便不能让其指向另一个对象,但是引用值是可以变化的
public static void main(String[] args) {
final int a=3;
a=1;//编译器提示:Cannot assign a value to final variable 'a'
final int[] ints={1,2,3,2};
ints[0]=9;//合法
ints=null;//编译器提示:Cannot assign a value to final variable 'a'
System.out.println(a);
}
String、StringBuffer、StringBuilder的区别及使用场景
String类:由于有final关键词修饰,是不可变的,在操作的时候都会产生新的对象,所以同样条件下会更加占用内存
StringBuffer类:由于成员方法均有synchronized修饰,所以其现线程安全,操作时也是在原有对象的基础上操作不会产生新的对象。下面源码我摘取了部分代码,我们可以看到方法均有synchronized修饰
@Override
public synchronized int length() {
return count;
}
@Override
public synchronized int capacity() {
return value.length;
}
@Override
public synchronized void ensureCapacity(int minimumCapacity) {
super.ensureCapacity(minimumCapacity);
}
/**
* @since 1.5
*/
@Override
public synchronized void trimToSize() {
super.trimToSize();
}
/**
* @throws IndexOutOfBoundsException {@inheritDoc}
* @see #length()
*/
@Override
public synchronized void setLength(int newLength) {
toStringCache = null;
super.setLength(newLength);
}
/**
* @throws IndexOutOfBoundsException {@inheritDoc}
* @see #length()
*/
@Override
public synchronized char charAt(int
StringBuilder类:没有synchronized修饰,线程不安全,操作时也是在原有对象的基础上操作不会产生新的对象
性能对比:string<stringBuffer<stringBuilder
使用场景:在操作字符时我们优先考虑stringbuilder,但是操作多线程共享变量时,因为存在线程安全问题,所以我们应该使用stringbuffer
重载和重写的区别
重载:重载发生在同一个类中,方法名相同,方法的入参不同,可以是顺序不同,类型不同,数量不同。方法的返回类型可以不相同也可以不相同
重写:重写发生在父子类中,方法名和方法入参要相同,返回类型、抛出异常的范围要小于大于父类,修饰符的范围要大于等于父类的修饰符,父类private修饰的方法不能被重写
接口和抽象类的区别
接口设计的目的,是对类的行为的一种约束,要求实现类有什么行为,不要去具体行为如何实现。有些类似于菜单,规定所有餐厅由一道宫保鸡丁,至于怎么做各个餐厅都有不同的做法
1、接口中的方法是没有实现的
2、只能使用public、final、static三种类型的成员变量
3、方法只能使用public abstrct修饰
4、接口能够有多个实现
抽象类:是对类的一种抽奖,解决代码复用的问题。比如有宝马、奥迪、奔驰三个类,这三种品牌的车均拥有发动机,续航,等相同的属性,我们把这些属性抽象出来,为Car类,宝马、奥迪、奔驰三个类继承Car类。公共的方法属性由抽象类实现,特殊个性化的行为属性由子类实现。
1、抽象类可以存在成员方法
2、抽象类中成员变量可以使用各种修饰符修饰
3、抽象类只能继承一个
List和set的区别
我们翻看过源码的话,可以知道list和set均是接口继承于collection接口
List:写入的数据是有序且可以重复,读取数据时可以通过迭代器读取数据,也可以通过get()方法通过下表index取数
有ArrayList、LinkedList 子类实现
Set:写入的数据是无序不能重复,读取数据时只能通过迭代器读取
有hashSet、LinkedHashSet实现类
ArrayList和LinkedList的区别
ArrayList 基于动态数组实现的,LinkedList基于双向链表实现。二者线程均不安全
鉴于二者实现的方式,有这么几点区别
1、ArrayList在查询数据时可以通过index下标获取,效率相较于LinkedList更高
2、新增数据时LinkedList效率要高于ArrayList
3、在新增和删除数据时,新增和删除的数据的位置不同二者的时间复杂度也有所不同。ArrayList新增或删除的位置在尾部时,时间复杂度时O(1),如果在特定位置时由于需要移动其他的元素,时间复杂度时O(n);LinkedList新增或删除的位置在尾部时,时间复杂度时O(1),如果在特定位置新增或删除元素,由于需要遍历元素,时间复杂度时O(N)
4、占用内存空间方面,ArrayList需要多余的空间预留,而LinkedList需要额外的内存空间存储前驱和后继
HashMap和HashTable和HashSet的区别
HashMap | HashTable | HashSet | |
---|---|---|---|
线程 | 不安全 | 安全 | 不安全 |
存放数据 | key-value | key-value | 非key-value |
hash值 | 通过key计算hash | 通过key计算hash | 通过value计算hash |
添加数据方式 | put方法 | put方法 | add方法 |
实现接口 | Map | Map | Set |
HashMap、HashTable底层均是有数组+链表+红黑树组成,以添加数据为例
HashSet底层是有HashMap实现的,add值是在key中存储,value是final修饰的Object对象。我们可以从下面的源码中看出
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
* Adds the specified element to this set if it is not already present.
* More formally, adds the specified element <tt>e</tt> to this set if
* this set contains no element <tt>e2</tt> such that
* <tt>(e==null ? e2==null : e.equals(e2))</tt>.
* If this set already contains the element, the call leaves the set
* unchanged and returns <tt>false</tt>.
*
* @param e element to be added to this set
* @return <tt>true</tt> if this set did not already contain the specified
* element
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
concurrentHashMap原理,jdk1.7和jdk1.8的区别
jdk1.7:
jdk1.7中,concurrentHashMap是由segment数组+HashEntry数组组成。从下面这个图我们更加清楚的看到一个Segment中含有一个HashEntry数组,每一个HashEntry又是一个链表结构
查询元素时需要经过两次hash计算,第一次需要定位segment的位置,再一次hash计算确定hashEntry的位置。
segment继承ReenTrantLock是一个分段锁,添加数据时,会锁着操作的segment,其他的segment不受影响,并发数为segment的个数,数组扩容也不影响其他的segment
jdk1.8
jdk1.8中,concurrentHashMap由synchronied+cas+node数组+红黑树组成
相较于HashTable成员方法添加synchronized的保证线程安全,concurrentHashMap是通过原子性和局部添加Synchronized保证线程安全,将能耗降到最低
截取了部分源码我们可以从源码中看到node类中val和next均由volatile修饰,通过volatile的修饰保证多线程中的可见性(这里不理解的可以看前面的文章;https://blog.youkuaiyun.com/qq_28039149/article/details/137150623)。
在查找、替换、赋值均使用CAS,当插入数据出现hash冲突时的后续操作,会有Synchronized修饰,这样的锁机制,锁的力度更小,效率更好。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
//添加数据的源码
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
java的异常体系
如上图,异常体系中最顶层是Throwable,两个子类,error和exception。
error是程序无法处理的异常,一旦出现程序会被迫停止运行,如我们常见的oom
exception不会导致程序停止。runtimexception发生在运行时如空指针,checkedexception出现在编译时候
gc如何判断对象是否可以被回收
垃圾回收机制有两种算法:1、引用计数法2、可达性分析法
1、引用计数法:每个对象都有一个属性标记引用的数量,新增一个引用加1,减少一个引用数值减1,当应用数量为0时,gc就会进行回收,但存在一个问题:如果A对象引用B对象,B对象引用A对象时,引用值一直为1,gc永远不会回收这个对象
2、可达性分析法:鉴于引用计数法可能存在问题,java使用可达性分析法作为垃圾回收的算法,从gc root开始遍历,只要能够搜索到的对象均不会回收,反之认为对象需要被回收。
那什么时gc root 呢?
(1)、虚拟机栈中引用的对象
(2)、方法区中静态属性引用的对象
(3)、方法区中常量引用的对象
(4)、使用native修饰的对象
线程的生命周期和线程的几种状态
我们通过上面的流程图我们看到线程的几种状态:new、runnable、waiting、timed_wait、blocked、terminated(结束状态)
sleep()、wait()、join()、yield()的区别
1、从上面的流程图我们不难看出不同点1是wait()等待的线程必须通过notify()或者notifyAll()才能唤醒,而sleep()计时结束后自动唤醒
2、方法的归属不一致,sleep()是Thred类的静态方法,而wait()是object类的成员方法,每个对象都有这个方法
3、wait()方法的调用必须先获取wait对象的锁,而sleep则没有这种限制
wait方法在执行完后会释放对象锁,其他线程可以正常获取该对象锁(我用完了,你们用吧)
sleep在synchronized修饰的代码块中执行时,不回释放对象锁,其他的线程不能获取对象(我睡500ms的时候抱着锁睡觉谁也不别想用)
yield():执行这个方法后,线程直接进入就绪状态,马上释放cpu的执行权,但是会保留cpu的可执行权限。所以cpi下次进行调度时,还会让这个线程继续执行
join():阻塞当前线程,执行完join的线程后,继续执行当前线程逻辑。如下面代码,主线程A,join线程t1后,不管t1线程休眠多久都会先执行t1的逻辑打印2222 再打印111111。如果我们把t1.join()方法注掉后,打印顺序就变为1111111 2222
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(2222);
}
});
t1.start();
t1.join();
System.out.println(111111);
}
输出结果
2222
111111
什么是死锁
所谓死锁,就是多线程运行过程中因抢夺资源造成的一种僵局,没有外力无法解决的情况
如上所示,死锁产生的四个条件分别是:
1、互斥条件,同一个同一时间只能有一个线程使用
2、请求并保持,线程申请锁收到阻塞时,不会自动释放目前占用的锁
3、不可剥夺,线程持有的锁只能自己使用结束主动释放,不能被别的线程剥夺
4、环路等待,线程A等待线程B持用的锁,线程B等待线程A持用的锁,形成一个闭合环路
那如何解决死锁问题呢?
我们上面说了死锁产生的四个条件,那解决这个问题可以从三个方面入手
资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)