Java基础篇
为什么要有字符流?
1、字符流就是字节流基础上加上编码形成的数据流
2、因为字节流操作中文时可能导致乱码(一个中文占2个字节)
3、Writer、Reader,常用FileWriter、FileReader、缓冲流:BufferdReader、BufferdWriter 转换流:InputStreamReader 、OutputStreamWriter (将字节流转换成字符流)
public class 读取不同编码的文本文件 {
public static void main(String[] args) throws IOException {
// BufferedReader br=new BufferedReader(new FileReader(new File("C:\\Users\\Administrator\\De
sktop\\6.txt")));
// 改进
BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream(new File("C:\
\Users\\Administrator\\Desktop\\6.txt")),"gbk"));
String st = br.readLine();
System.out.println(st);
}
}
4、字节流可以支持声音视频等所有文件类型,字符流只支持文本
序列化和反序列化
1、对象—>字节序列:序列化
字节序列—>对象:反序列化
2、作用:
-
将对象保存到硬盘中
-
在网络上传输对象的字节序列
比如我现在要把对象的字节序列保存到硬盘中,不实现序列化接口就会报错:
Exception in thread "main" java.io.NotSerializableException: com.linht.redis.testserializable.User
代码如下:
User user=new User();
user.setName("abc");
ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream(new File("D:\\User.txt")));
objectOutputStream.writeObject(user);
System.out.println("==================");
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(new File("D:\\User.txt")));
User o = (User) objectInputStream.readObject();
System.out.println(o);
简单实现的jdk动态代理
接口
public interface IService {
public void fun();
}
public class ServiceImpl implements IService {
@Override
public void fun() {
System.out.println("dosomething......");
}
}
测试类
IService service=new ServiceImpl();
IService o = (IService) Proxy.newProxyInstance(service.getClass().getClassLoader(), service.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before....");
Object invoke = method.invoke(service, args);
System.out.println("after...");
return invoke;
}
});
o.fun();
}
String和其他基本数据类型的包装类都是final class
//Integer类型
public final class Integer extends Number implements Comparable<Integer> {
//String类
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
其中String底层是一个final修饰的字符数组:
private final char value[];
接口的特点
1、完全抽象,没有任何具体实现。方法没有方法体
2、接口的域都是隐式的final和static
3、接口中的方法都是默认的public,abstract
4、接口没有构造方法-----但是抽象类是可以有构造方法的----应该是因为要继承吧,子类必须调用父类的构造方法
5、接口不能实例化----这个跟抽象类一样,抽象类产生的根本原因就是为了不让产生该类的任何对象
6、支持多继承(一个类可以实现多个接口)
接口与抽象类
接口 | 抽象类 | |
---|---|---|
构造器 | 无 | 有 |
方法 | 都是抽象 | 可以有非抽象方法 |
成员变量 | 实际是常量 | private、默认、protected、public |
内部类
有个很好的例子就是迭代器:(注意下面这个是ArrayList的成员内部类)
//调用iterator()就是返回内部的迭代器
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
///内部有个指针cursor,初始化为0 ,每调用一次next()就+1
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
//使用外部类.this,引用外部类对象
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
//也是调用remove()方法,只是会去修改expectedModCount
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
方法值传递
Java 语言的方法调用只支持参数的值传递
public class Test1 {
public static void main(String[] args) {
MyBean2 ptr1=new MyBean2();
ptr1.setAge(9);
test(ptr1);
System.out.println(ptr1);
}
//注意这里是复制了一份引用,跟ptr1指向同个对象
public static void test(MyBean2 ptr2){
//引用指向另一个对象了
ptr2=new MyBean2();
ptr2.setAge(10);
System.out.println(ptr2);
}
}
class MyBean2{
private int age;
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "MyBean2{" +
"age=" + age +
'}';
}
}
====
运行结果:
MyBean2{age=10}
MyBean2{age=9}
集合中的删除方法
看下ArrayList的remove方法,先把删除元素往后的元素数组,整个往前移一位,然后把最后那个元素置空。
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
置空这一步应该是为了防止内存泄漏,所有的数据结构在删除元素的时候都会考虑这一点,Stack其实也有:
public synchronized void removeElementAt(int index) {
modCount++;
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " +
elementCount);
}
else if (index < 0) {
throw new ArrayIndexOutOfBoundsException(index);
}
int j = elementCount - index - 1;
if (j > 0) {
System.arraycopy(elementData, index + 1, elementData, index, j);
}
elementCount--;
elementData[elementCount] = null; /* to let gc do its work */
}
对象克隆
java中需要克隆的类,需要实现cloneable接口,并重写Object的clone()方法
注意这里的cloneable接口和clone()方法没有任何关系
示例:
public class A implements Cloneable{
private String name;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();//Object这个方法是native方法,能实现对象的拷贝
}
public static void main(String[] args) throws CloneNotSupportedException {
A a=new A("A");
A clone = (A)a.clone();
System.out.println(a);
System.out.println(clone);
}
实现接口是必须的(否则会报错,但其实这个接口是个空壳,clone()方法也不是它的,这个接口应该只是一个标记)
这里有个疑问,为什么要覆盖clone方法?
如果不覆盖的话,假设当前类叫Car,我们new Car(),没覆盖clone方法
此时调用clone方法就是相当于调用Object类的
当然在Car类内部任何方法都能访问clone,毕竟是Object的子类嘛
但是其他类就都不行了呀,因为我们自定义的类肯定都不是跟Object在同个包里面
Object类中的clone方法是一个native方法:(一开始看没有方法体还以为是个抽象方法,其实不是,毕竟Object不是抽象类)
protected native Object clone() throws CloneNotSupportedException;
由于clone方法是本地方法,所以效率会很高,总结下实现对象克隆有几点:
1、实现cloneable接口,否则会抛出异常
2、重写clone()方法,并把访问权限提升为public(这样不同包下的其他类才能访问到这个方法)
示例:
class CloneClass implements Cloneable{
public int aInt;
public Object clone(){
CloneClass o = null;
try{
o = (CloneClass)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return o;
}
}
深拷贝和浅拷贝
浅拷贝:被复制对象的所有值属性都与原来对象的相同,而所有的对象引用属性仍然指向原来的对象。
深拷贝:在浅拷贝的基础上,所有引用其他对象的变量也进行了clone,并指向被复制过的新对象。
说到底就是引用对象上的区别
默认的clone()方法是浅拷贝
深拷贝浅拷贝代码:
public class Test2 {
public static void main(String[] args) throws CloneNotSupportedException {
A a=new A();
B b=new B();
b.setAge(10);
a.setB(b);
//默认是浅拷贝
A cloneA =(A) a.clone();
System.out.println("a==="+a);
System.out.println("cloneA====="+cloneA);
//修改了a 的B对象,结果把克隆对象的B对象也改了
a.getB().setAge(11);
System.out.println("a==="+a);
System.out.println("cloneA====="+cloneA);
==========================
a===A{b=B{age=10}}
cloneA=====A{b=B{age=10}}
a===A{b=B{age=11}}
cloneA=====A{b=B{age=11}}
}
}
class A implements Cloneable{
private B b;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
public void setB(B b) {
this.b = b;
}
public B getB() {
return b;
}
@Override
public String toString() {
return "A{" +
"b=" + b +
'}';
}
}
class B{
private int age;
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "B{" +
"age=" + age +
'}';
}
}
解决办法之一就是B也实现cloneable接口,覆盖clone方法提升为public,然后在A的clone方法中克隆B,再把克隆对象设置到A的克隆对象中
public class Test2 {
public static void main(String[] args) throws CloneNotSupportedException {
A a=new A();
B b=new B();
b.setAge(10);
a.setB(b);
//默认是浅拷贝
A cloneA =(A) a.clone();
System.out.println("a==="+a);
System.out.println("cloneA====="+cloneA);
//修改了a 的B对象
a.getB().setAge(11);
System.out.println("a==="+a);
System.out.println("cloneA====="+cloneA);
=============
a===A{b=B{age=10}}
cloneA=====A{b=B{age=10}}
a===A{b=B{age=11}}
cloneA=====A{b=B{age=10}}
}
}
class A implements Cloneable{
private B b;
@Override
public Object clone() throws CloneNotSupportedException {
B cloneB= (B) b.clone();
A cloneA= (A) super.clone();
cloneA.setB(cloneB);
return cloneA;
}
public void setB(B b) {
this.b = b;
}
public B getB() {
return b;
}
@Override
public String toString() {
return "A{" +
"b=" + b +
'}';
}
}
class B implements Cloneable{
private int age;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "B{" +
"age=" + age +
'}';
}
}
匿名内部类
用途:当我们只需要用某一个类一次时,且该类从意义上需要实现某个类或某个接口,这个特殊的扩展类就以匿名内部类来展现。
匿名内部类是否可以继承其他类,是否可以实现其他接口?
匿名内部类在实现时必须借助一个接口或者一个抽象类或者一个普通类来构造,从这个层次上讲匿名内部类是实现了接口或者继承了类,但是不能通过extends或implement关键词来继承类或实现接口。
用法示例:
public class Pacel7 {
interface Contents{}
public Contents contents(){
return new Contents() {
private int i =11;
public int value(){return i;}
};
}
}
代码中常用到的:比较器,如:
Map<Integer,String> map=new TreeMap<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return (o2.intValue()-o1.intValue());
}
});
Collection与Collections
Collections是个工具类,Collection是个集合接口 (List接口、Set接口都直接继承于Collection接口)
Collections有个比较常见的用法就是对集合线程安全化,比如:
java.util.List list=new ArrayList();
Collections.synchronizedList(list);
源码大致如下:
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
static class SynchronizedList<E>
extends SynchronizedCollection<E>
implements List<E> {
private static final long serialVersionUID = -7754090372962971524L;
final List<E> list;
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
.................
public boolean equals(Object o) {
if (this == o)
return true;
synchronized (mutex) {return list.equals(o);}
}
public int hashCode() {
synchronized (mutex) {return list.hashCode();}
}
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
.....................
}
可见,返回的是个SynchronizedList类对象,该类中,在调用原有List的方法时,都加了一个 synchronized (mutex) ,来实现线程安全化(就是把原有集合对象给包了一层)
集合类图
Iterator也是一个接口!
List接口
有序 ,三个实现类:
-
ArrayList :数组实现,适合遍历查询,不适合插入删除
-
Vector :数组实现,支持线程同步
-
LinkedList:链表,适合插入删除
Set接口
独一无二
- HashSet:个人认为跟HashMap一样
其实HashSet内部维护了一个HashMap,在add方法时,调用的是map的put方法,加入的元素当作key,value为一个Object对象静态常量。
- TreeSet:底层是红黑树
底层又是TreeMap,TreeMap特点就是存储的键值对按照键来排序,底层用红黑树实现
Map接口
TreeMap 取出来的是排序后的键值,HashMap 的键值对在取出时是随机的
注意,TreeMap比较的对象是Key,所以Key必须实现Comparable接口,否则要传入比较器**,像Integer和String 可以进行默认的TreeMap排序,是因为他们都实现了Comparable接口
TreeMap:红黑树
看TreeMap的put方法:来观察它是怎么用的二叉树:
public V put(K key, V value) {
Entry<K,V> t = root;
//如果根节点为空,把新加入的元素当前根节点
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
//比较器不为空
if (cpr != null) {
do {
//先拿key与根节点key比较,小于根节点,则继续与左子节点比较
//大于根节点,则继续与右子节点比较
//如果key相等,那么就把该节点的值更新为传进来的值,return回去
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
//比较器为空,同样是跟根节点比较的那套逻辑
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
//key必须实现Comparable接口
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
//到这里,说明没找到与key相等的那个节点,此时t应该指向一个空节点,parent是他的父节点
//新建一个节点
Entry<K,V> e = new Entry<>(key, value, parent);
//key比父节点小,放左边,否则放右边
if (cmp < 0)
parent.left = e;
else
parent.right = e;
//新的元素安置好后,需要对树进行调整
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED;
while (x != null && x != root && x.parent.color == RED) {
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
root.color = BLACK;
}
底层红黑树
但为什么要变成红黑树呢?
红黑树的应用比较广泛,主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率非常之高。
为什么不用平衡二叉树呢?
红黑树不是完全平衡的,但是保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单
所以红黑树的优点:1、接近平衡:使得搜索效率高 2、不完全平衡:使得插入操作容易实现,旋转次数少
反射
1、反射创建对象
2、反射创建对象效率比new 低,因为要先查找类资源、使用类加载器创建。
典型例子
Class.forName(‘com.mysql.jdbc.Driver.class’);
怎么用
所有获取对象的信息都需要Class类来实现,所以首先就是要拿到class对象,有这些方法可以拿到:
1、Class.forName(“类全路径”)
2、对象.getClass()
3、类名.class
反射创建对象实例
1、要求有 无参构造:(否则抛出InstantiationException)
LoadDataClass loadDataClass = LoadDataClass.class.newInstance();
System.out.println(loadDataClass);
2、有参构造:先使用 Class 对象获取指定的 Constructor 对象,再调用 Constructor 对象的 newInstance()
Constructor<LoadDataClass> declaredConstructor = LoadDataClass.class.getDeclaredConstructor(int.class);
LoadDataClass loadDataClass = declaredConstructor.newInstance(19);
System.out.println(loadDataClass);
单例模式DCL为什么要用Volatile关键字
pulic class myClass{
private myClass(){}
private static volatile myClass intsance;
public static myClass getInstance(){
if(instance==null){
synchronized(myClass.class){
if(instance==null){
instance=new myClass();
//这一步:
1、分配内存空间
2、调用构造函数
3、把内存地址赋给引用
但指令重排可能会先3后再2,导致这个实例还没有实例化,就被引用了
}
} } }}
StringBuffer和String
StringBuffer部分源码
1、先看构造方法,底层就是一个char[]数组(这个跟String一样),默认长度为16
public StringBuffer() {
super(16);
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
2、看append(String)方法:
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
public AbstractStringBuilder append(String str) {
//如果是个null(不过不能直接append(null)),那么会直接加上null
if (str == null)
return appendNull();
int len = str.length();
//数组长度不够的话,就扩容
ensureCapacityInternal(count + len);
//把参数拷贝到char[]后面
str.getChars(0, len, value, count); //这里调用的是String的方法
//元素个数
count += len;
return this;
}
下面这个是String的方法,就是把要append的String对象的字符数组,拷贝到StringBuffer对象的字符数组后面。
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
像String的+操作,会生成一个新的String对象,不过这里的append就不会,直接把String参数拷贝到char[]数组后面
另外,StringBuilder就是方法都不加synchronized而已
=================
在字符串拼接的时候,String对象用的是+这么一个重载的操作符,而StringBuiler则提供了一个append方法。
String在使用+操作符时,编译器也会自动为我们生成一个StringBuilder对象,调用append方法进行拼接,最后调用toString方法返回结果。
但是如果是在循环里面使用String的+,就会导致每次循环都创建一个StringBuiler,使用StringBuilder,我们自己就只会创建一个,还能自己控制初始容量大小等。
Stream流式编程
处理集合
优点:
- 代码简洁 简化并行程序编写
操作符:
- 1、中间操作符
- 2、终止操作符
flatmap与map
-
map:
接受一个函数作为参数,这个函数会被应用到每个元素上,并将其映射成一个新的元素对流中每个元素进行处理 -
flatmap:
各个数组并不是分别映射成一个流,而是映射成流的内容,所有使用map(Arrays::Stream)时生成的单个流都被合并起来,即扁平化为一个流,流扁平化,让你把流中的每个值都转换成另一个流,然后把所有的流连接起来成为一个流
项目中的应用:
1、用来格式化异常输出:
private String bindResult2str(BindingResult br){
return br.getFieldErrors().stream()
.map(e->"[" + e.getField() + "]:" + e.getDefaultMessage())
.collect(Collectors.joining(";"));
}
HashMap
get(Object key)方法:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
上述get方法作用是根据key找到值
过程:
1、先根据key取出Node节点(Map里面存储的都是Node节点,node节点的结构见上面)
(1)先生成key的hash
(2)根据hash值定位到第一个Node节点(比较第一个节点的hash和传入key的hash,以及key是否相同或者equals,是的话就返回这个节点)
(3)否则比较下一个节点,同样比较key的hash值和node的hash值,以及key跟Node的key是否相同或者equals
总而言之,key的hash(hashcode方法)是为了定位在哪个槽,key是为了逐个比对(调用key的equals方法)到底是哪一个。
2、返回node节点的value
为什么寻槽的时候要用:tab[(n - 1) & hash],因为不管hash值多大,只要跟n-1做&运算,那么结果一定是小于等于n-1的,这样就保证槽下标不会超出n-1
put方法:
//插入新的值,返回的是旧的值
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果槽的空的,就扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果定位到的槽是空的,就new一个node,放到槽里面,完成
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//槽非空
else {
//这个e就是最终定位到的,我们要更新值的那个Node
Node<K,V> e; K k;
//对比下要插入的key的hash和槽首节点的hash,以及key与槽首节点的key是否相同(内存地址或者equals)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
HashMap如何Put:
1、通过计算key的hash值,&上表长度-1,定位到哈希桶
2、如果桶是空的,直接新建一个节点放进去
3、如果桶非空:
(1)判断key是否==或者key的equals()方法是否相等,是的话说明key存在,覆盖原有节点的值
(2)判断当前是否是红黑树,是的话,以红黑树的逻辑加入节点
(3)循环比对下一个节点的Key,一致的话,覆盖值,否则在链尾插入新的节点
HashMap扩容
第一次初始化扩容是16
不是第一次扩容,则容量变为原来的2倍,以2的幂次方扩容
何时扩容(键值对数量超过阈值的时候)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if (++size > threshold)
resize();
在put方法里面有两个地方扩容,一个是第一次put的时候,一个是size(这个size在每次put一个新的键值对时自增1,也就是标志着map的键值对数量)大于threshold时
threshold=capacity*loadFactor
HashMap初始化
初始化时不给定容量
像我们平时写的:Map map=new HashMap();
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted 0.75f
}
只初始化了一个负载因子,其他默认,此时 table为null , threshold为0
此时,我们调用put方法,
初始化时给定容量
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);//0.75f
}
public HashMap(int initialCapacity, float loadFactor) {
//不得小于0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//不得超过最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//负载因子必须大于0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//阈值根据指定容量来计算
this.threshold = tableSizeFor(initialCapacity);
}
返回大于输入参数且最近的2的整数次幂的数
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1; 无符号右移,忽略符号位,空位都以0补齐
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
put方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
扩容方法resize
1、new HashMap()或者new HashMap(n)实际上table都是null,只有put的时候才去创建Node数组,前者创建长度为16,后者创建长度为大于且最近n的2的倍数
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//旧表的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧表的阈值
int oldThr = threshold;
//新表的长度和阈值都初始化为0
int newCap, newThr = 0;
//如果旧表长度大于0(肯定不是刚new的,而是调用过Put方法了)
if (oldCap > 0) {
//如果旧表长度都已经超过最大容量了,把旧表返回去,不再扩容了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//旧表长度>=16,且扩容一倍后新表长度不会超过最大值
//这就是正常情况的扩容嘛,此时就把新表的阈值设置为旧表的2倍,新表的长度也设置为旧表的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//new HashMap(n)时调用第一次put方法
//旧表长度为0,但旧表阈值>0 ,此时将旧表阈值赋值给新表的长度(如果初始化时给到容量,那么此时oldThr就是大于且最接近给定容量的2倍数)
else if (oldThr > 0)
newCap = oldThr;
///new HashMap()时调用第一次put方法
else {
//旧表长度为0,阈值也为0,这就是第一次扩容嘛,新表长度初始化为16,新表阈值初始为 16*0.75
newCap = DEFAULT_INITIAL_CAPACITY; //16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //0.75*16
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//上面是在判断新的容量应该为多少
@SuppressWarnings({"rawtypes","unchecked"})
//新建一个新容量的Node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果是第一次put,这里oldTab为空不进入,就相当于只是新建了一个长度为16的Node数组
if (oldTab != null) {
//循环整个Node数组,也就是整个旧表的每个哈希槽
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//只有当前哈希槽不为空,才进行if语句里面的操作,否则跳过,毕竟是空嘛,不需要复制
//先取出当前哈希槽的第一个节点
if ((e = oldTab[j]) != null) {
//把这个哈希槽置空(我估计是为了让jvm回收)
oldTab[j] = null;
//如果下一个Node节点是空,说明当前哈希桶只有一个节点,那么就直接把这个节点复制到新的哈希表对应的位置
//注意这里到了新哈希桶的位置,是那该节点的hash去&上新哈希桶的长度-1,就是说在旧哈希表里面可能是第一个槽,
//到了新的哈希表就不一定了,哈希槽下标会变化的意思。。。。
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果是红黑树,。。。
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//否则,就说明是个链表了
else { // preserve order
//都是Node节点
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//循环当前哈希槽的整个链表
do {
next = e.next;
//如果hash跟旧表长度相与为0 ,说明在旧表长度二进制数的最高位一定是0,比如10000
//那么hash值从右算起第5位也一定是0,比如hash值为:111101111
//该hash值在原来表中的下标是111101111 & 1111 =1111,也就是取后4位
//该hash值在扩容一倍后的表的下标是111101111 & 11111 =01111,也就是取后5位
//就是说,同个hash值,在扩容一倍前,与扩容一倍后的表中的下标,区别在于最高位
//如果最高为是0 ,那么这个hash值,在扩容前后的表中的下标不会改变
//如果会改变,说明最高位是1,而因为后面都一样,所以实际上新的散列下标是:旧的散列下标+旧表的长度
//这就是为什么这个if判断的是那些散列值不改变的节点
//也是为什么最下面写newTab[j + oldCap] = hiHead;的原因
//散列下标不变的节点, 请注意这里是oldCap,不是oldCap-1
//就是说,在同个哈希槽里面的节点,他们的key hash的二进制数后4位一定是相同的(假设旧表长度是16)
//但第5位就不一定相同,有的0有的1,但也只有0 1之分,没有其他情况,所以这样要把他们拆成两个链表
//当然要拆了,不然hash值与上新表长度-1,肯定是不同的,不能再放到同个哈希桶里面
//所以这里就搞了4个指针,分别指向第5位为0 的节点组成的链表的头尾、第5位为1的节点组成的链表的头尾
//而且这里注意,在当前表里面后4位跟其他哈希桶的节点不一样,那么到了新的表,也不需要考虑会跟其他哈希桶的节点发生冲突了,一定是要么放到原来的桶位置,要么放到新的桶位置
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//散列下标会改变的节点
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//遍历完当前哈希槽的链表了
//loTail是链表的最后一个节点
if (loTail != null) {
loTail.next = null;
//当前哈希槽指针指向链表的第一个节点
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
为什么hashmap的长度为2的倍数?
因为hashmap里面计算散列下标是这样的:
hash & tab.length -1
如果tab.length不是2的倍数,那么tab.length-1 的二进制就不可能全是1,比如10000000这种比较极端的情况下,那么很多值,&上10000000,就会产生相同的值,这样hash冲突就变多了
如果是2的倍数,那么tab.length-1的二进制一定全是1,那么只要hash值不同,就不会产生冲突。
ThreadLocal内存泄漏
个人理解:调用threadlocal.set(Object)时, Thread中维护一个ThreadLocalMap,key指向一个ThreadLocal对象,是弱引用,value指向我们要存入的对象。
1、如果是像线程池那样,线程不会被回收,那么value指向的对象这条引用链就一直存在,结果导致内存泄漏
2、如果key强引用threadLocal对象,那么threadlocal对象会一直无法回收,也导致内存泄漏(所以key才需要用弱引用)
所以,第一是弱引用threadlocal对象,让他能被回收,第二是使用后要记得调用threadlocal的remove方法,及时释放value的引用。
ThreadLocalMap结构:
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
......
ArrayList往中间插入一个值
把目标位置后面的元素都往后移,再把新元素填进来
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
//这个是一个Native方法
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
LinkedList往中间插入一个值
找到目标位置的ndoe节点,更改指针指向
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
Node<E> node(int index) {
//如果是插入下标在链表前半部分,从头往后找
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
}
//如果是后半部分,从后往前找
else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
equals()
Object类的方法:
public boolean equals(Object obj) {
return (this == obj);
}
1、所有类都有equals方法,因为所有类都继承于Object
2、如果没有重写该方法,也就是默认是比较对象的内存地址
3、举个重写了该方法的类: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;
}