基础
Exception
- 当
try
或catch
语句块中有 return 语句时,先执行try
或catch
语句块中 return 语句之前的代码,再执行finally
语句块中的代码,之后再返回。所以,即使在try
或catch
语句块中有 return 语句,finally
语句块中的代码仍然会被执行。 - throw:手动抛出异常
- throws:将方法中的异常抛给调用该方法的对象
public class Test2 {
public static void main(String[] args) {
try {
System.out.println("111");
throw new NullPointerException("空指针异常");
}catch(ArrayIndexOutOfBoundsException e) {
System.out.println(e);
}
}
}
异常对象中传的参数是异常信息(e.getMessage())
-
自定义异常:当我们需要自定义一个异常时只需要将我们自定义的异常类去继承一个异常类就可以了
public class MyException extends RuntimeException{ public MyException(String msg) { super(msg); } }
异常分为运行时异常和编译(检查)时异常,当一个异常类继承了RuntimeException这个类则说明这个异常类就是运行时异常,否则为编译时异常
finally
-
finally关键字内的内容不管是否发生异常都会被最终执行,如果在try之前发生异常或者在try块或者catch块中停止了jvm则不执行
-
当try、catch中存在return语句时,finally中的内容会在return语句执行之前执行,执行完了finally中的内容再return
-
特殊情况:finally中存在return时,会返回finally的值
-
finally中的代码不被执行的情况:
- System.exit(); JVM虚拟机被终止
- 在try之前发生了异常
final
-
被final修饰的类不可以被继承
-
被final修饰的属性为常量
-
被final修饰的方法不可以被重写
- 注意:
上面我们说被final修饰的方法不可被重写,那看看下面的代码
public class Person { private final void test() { } } class Student extends Person{ private final void test() { } }
这里编译通过,运行也没出现异常,那这个是否是重写了方法呢?
因为在父类中的test()方法是用private关键字修饰的,所以当子类继承了父类,但是这个被private修饰的方法对子类并不可见,所以这并不是重写。
finalize
- 通知垃圾回收器回收垃圾
flush
- 将缓冲区中的内容刷新(强制写出),当调用close()方法时也会调用
static关键字
-
局部变量只能用final修饰
-
一个类中可以有多个static代码块
-
一个类中可以有多个代码块
-
当一个类中同时有代码块和static代码块,初始化这个类的一个对象时,
构造方法、代码块、static代码块、static修饰的成员变量赋值、普通成员变量赋值执行顺序:
如果static修饰的成员变量在static代码块前面则先执行static修饰的成员变量赋值,否则反之
如果普通的成员变量在代码块前面则先执行普通饰的成员变量赋值,否则反之
属性初始化->static修饰的成员变量赋值->static代码块->代码块->构造方法
public class TestStatic { public int i=1; { System.out.println("普通成员变量赋值:i ==" +i); i=10; System.out.println("普通代码块赋值:i ==" +i); } public static int j=1; static { System.out.println("static成员变量赋值:j ==" +j); j=10; System.out.println("static代码块赋值:j ==" +j); } public TestStatic(){ System.out.println("构造方法"); } public static void main(String[] args) { new TestStatic(); } }
执行结果:
static成员变量赋值:j ==1
static代码块赋值:j ==10
普通成员变量赋值:i ==1
普通代码块赋值:i ==10
构造方法 -
staitc代码块、方法中只能调用static修饰的方法和成员变量,不能使用super、this关键字
-
一个类中static修饰的方法可以被继承并通过子类的类名调用,但是不能被重写。
super关键字和this关键字
super
- super关键字指的是父类的对象
- 如果需要通过super关键字调用父类的构造方法只能在子类构造方法的第一行中调用
- 可以通过super关键字调用父类的属性和方法
- super关键字不能在static修饰的代码块或者方法中使用
- 如果我们不显式的使用super关键字调用父类的构造方法并且不使用this关键字调用构造方法,则编译器会给我们默认提供super()调用父类的无参构造
this
- this关键字是当前类的对象的引用
- 可以在构造方法中通过this关键字调用当前类中其他的构造方法,只需要填写与构造方法相同的参数列表的参数即可
- 可以使用this关键字区别同名的成员变量和局部变量
- this关键字不能在static修饰的方法或者静态代码块中使用
注意:
-
this和super指向的都是子类对象,也就是当前类的对象
-
在子类构造方法中调用super()并没有创建一个父类对象,只是在子类对象的区域内初始化了父类的特征
参考:
https://blog.csdn.net/championhengyi/article/details/78778575
https://blog.csdn.net/qq_44715943/article/details/115916373
方法重载
- 重载方法:方法名一样,参数列表不同,和返回值、访问权限修饰符无关
- 参数列表不同:参数个数不同、参数类型不同、参数顺序不同
- 方法重载是方法的扩展
- 方法重载在同一类中发生
- 方法重载是静态绑定
方法重写
- 重写方法名要和被重写方法名一致
- 返回值与被重写方法的返回值一致,或者是被重写方法返回值的子类
- 重写方法不能缩小被重写方法的访问权限
- static修饰的方法不能被重写
- 被final关键字修饰的方法不能被重写
- 重写方法的参数列表和被重写方法的参数列表一致(参数类型是被重写方法参数类型的子类也不行)
- 方法重写发生在子类继承父类中,或者实现接口中
- 方法重写是对方法的覆盖
- 方法重写是动态绑定,也是多态
接口
-
JDK1.8之前接口中的方法全是抽象的,没有用public abstract修饰的方法编译器会自动帮我们在方法前加上,字段都为常量。
-
JDK1.8之后(包含1.8)可以有默认的方法和静态方法。
-
注意:1.8之后的静态方法只能通过接口名来调用,不能通过实现该接口的类名调用,接口的实现类没有接口中的静态方法,接口中的静态方法不能被别的接口继承
-
接口没有构造方法
-
实现了接口的类拥有接口中定义的常量
-
接口只能继承接口,并且能继承多个接口
抽象类
- 一个类中有抽象方法则这个类就是抽象类,一个抽象类中可以没有抽象方法,可以有普通方法。
- 抽象类有构造方法,但是不能被直接初始化。
- 抽象类中可以存在代码块(构造代码块、静态代码块)
- 抽象类中可以存在静态方法
- 抽象类中的属性和普通类的属性一样
继承
- 一个类通过extends关键字继承一个类,被继承的类称为基类或超类,子类称为衍生类。
- 当子类中有和父类相同的变量时,不会覆盖父类的。
- 父类中有静态方法/属性时,子类可以通过子类的类名调用父类中的静态方法/静态属性(接口中实现类不能通过实现类的类名调用接口中的静态方法)
- 一个类只能直接继承一个类,这是java的单继承,但是一个接口可以继承多个接口,所有的类的基类是Object类
- 当子类继承父类并重写了父类的方法,那么当父类的引用调用被重写的方法时会调用子类的方法。
- 当子类中有和父类相同名字的属性,当父类的引用调用同名的属性时,调用的是父类的,因为属性不会被覆盖。
- 子类的构造方法如果没有显式的使用super关键字调用父类的构造方法和使用this关键字调用其他的构造方法则编译器会默认的提供super()用于调用父类的无参构造方法,否则默认器不会提供。
匿名内部类
-
匿名内部类就是没有名字的类
public interface IAnimal{ void eat(); } public class Demo01 { public static void main(String[] args) { /*IAnimal animal = new IAnimal(){ @Override public void sleep() { System.out.println("睡觉"); } }; animal.sleep();*/ //将匿名内部类的对象直接当作参数传递给方法 method(new IAnimal() { @Override public void sleep() { System.out.println("睡觉"); } }); } public static void method(IAnimal animal){ animal.sleep(); } }
静态内部类
-
静态内部类的定义格式
public class OutClass { public static String name; public static class InnerClass{ public String name; public void show(String name) { System.out.println("参数name:"+name); System.out.println("内部类name"+this.name); System.out.println("外部类name:"+OutClass.name); } } }
-
创建静态内部类的方式
InnerClass ic=new OutClass.InnerClass();
-
静态内部类中不能调用外部类中非静态的方法和属性
-
静态内部类中调用外部类中静态的方法或属性的格式 : 外部类名.方法名/属性名
-
静态内部类的创建无需依赖外部类对象(所以创建内部类对象时不需要创建外部类对象)
成员内部类
-
成员内部类的定义格式
public class OutClass { public String name; public class InnerClass { public String name; public void show(String name) { System.out.println("方法的name:"+name); System.out.println("内部类的成员name:"+this.name); System.out.println("外部类成员name:"+OutClass.this.name); } } }
-
成员内部类可以调用外部类的所有属性和方法(包括static、final、private修饰)
-
创建成员内部类的方式
OutClass.InnerClass ic=new OutClass().new InnerClass();
-
内部类中和外部类中存在同名的属性或方法时,可以通过外部类名.this.属性名/方法名调用外部类的属性或方法
-
在内部类中的this指的是内部类对象的引用
局部内部类
-
局部内部类定义格式
public class OutterClass { public String name; public void show() { class InnerClass{ public String name; public void test() { System.out.println("外部类的name:"+OutterClass.this.name); System.out.println("内部类的name:"+this.name); } } InnerClass ic=new InnerClass(); ic.test(); } public static void main(String[] args) { OutterClass oc=new OutterClass(); oc.show(); } }
-
局部内部类只能在定义局部内部类的方法中创建
-
局部内部类和局部变量相似,只能用final修饰局部内部类
-
局部内部类可以调用外部类中的所有属性和方法
集合
HashSet
-
无序(存放元素的元素和取出的顺序不同),不可以存放相同的元素(通过hashCode()方法和equals()确定元素是否相同(当位置确定了就取决于存储的对象的equals()是怎么实现的))
-
底层实现是HashMap(jdk8之前是哈希表+单链表,8包括8之后是哈希表+单链表+红黑树),所以特点和HashMap集合差不多,可以存放null,并且在哈希表的第一个位置
-
HashSet集合也不能在foreach中对使用集合的remove()方法对集合进行移除,会报并发修改异常,因为foreach实际上就是用的iterator来进行遍历的,iterator的next()方法会对modCount属性进行校验,和ArrayList集合原理一样,HashMap的迭代器是HashIterator类创建的对象
-
可以存放null
-
通过无参构造创建集合对象时,容量为0,在第一次添加元素时扩容为16
-
每次扩容为原来的两倍
public HashSet() { map = new HashMap<>(); } public HashSet(Collection<? extends E> c) { map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); } public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); } public HashSet(int initialCapacity) { map = new HashMap<>(initialCapacity); } HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
Vector
-
底层是一个Object类型的数组
-
是线程安全的,方法被synchronized关键字修饰
-
扩容机制:Vector集合中有一个属性是capacityIncrement,它是用于存放集合每次扩容的大小,如果不手动赋值,则为0,可以自己设定每次扩容的大小,不设置就默认是原来的一倍。
Vector集合的所有构造方法:
//设定初始容量,并设置每次扩容的大小
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
//设置初始容量,每次扩容的大小为原来的一倍
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
//默认初始容量为10
public Vector() {
this(10);
}
Vector集合的扩容方法:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//判断有没有设置每次扩容的大小,如果设置了采用设置的,没有就增加为原来的一倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
//如果扩容后的容量不满足数组的最低要求,则采用数组的最低要求作为新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果新容量超过了数组容量的最大容量为Integer的最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
可以看到它新的容量首先是根据int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);来确定。capacityIncrement 这个是我们在创建Vector时可以确定的。如果不传这个值就默认是0,所以每次扩容是原来的一倍。
ArrayList
-
底层:Object类型的数组
-
扩容机制:每次扩容为原来数组长度的1.5倍
-
ArrayList集合特点:可以存储重复元素,有序,存入进去的顺序和取出来的顺序一样,可以存储null
-
如果使用默认的无参构造时,它存储元素的数组长度为0,在添加第一个元素时进行扩容,扩容后的容量(数组长度)为默认的初始容量10
-
ArrayList集合的contains()判断一个元素是否存在的依据是通equals()方法,是否存在就看传入的类型的equals方法是如何实现的
-
当通过传递int类型的构造方法创建集合对象时,如果传递的是0,那么在添加第一个对象时将会进行扩容,扩容后的容量是1
//设置数组初始容量 public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } //判断数组是不是默认的空数组,如果是,它扩容的容量为10,不是则为原来容量的1.5倍 private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } //判断是否达到了扩容要求 private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } //进行扩容,扩容为原来的1.5倍,如果1.5倍没有达到数组最小要求,则直接把数组需要的大小作为扩容的新容量 private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
-
不能在foreach中进行使用ArrayList集合的remove()方法进行移除元素,通过iterator迭代器迭代遍历元素时可以移除元素,不能用增强for,因为用增强for的时候它会生成一个iterator迭代器,调用的是iterator的hasNext()、next()方法,在集合里面有一个Itr内部类,在ArrayList集合的父类中有一个变量modCount,它的作用是记录集合结构被修改的次数,在创建内部类对象的时候它会赋值给内部类的expectedModCount这个成员变量中,当调用集合的remove()方法时modCount会自增,当调用iterator对象的next()方法时它会检查初始化时会调用checkForComodification()检查存储的expectedModCount和当前的modCount是否相等,很显然是不等的,所以会抛出ConcurrentModificationException异常。
private class Itr implements Iterator<E> { //next()和hasNext()共同的指向 int cursor; // index of next element to return //最后一次返回的元素的索引 int lastRet = -1; // index of last element returned; -1 if no such //对集合的操作次数 int expectedModCount = modCount; public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { //实际上iterator对象的remove方法还是调用的集合的remove方法,只不过它执行完了会更新expectedModCount的值,从而保证了next()方法 下一次调用时checkForComodification()的正确性 ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; //更新对数组结构操作的次数 } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } @Override @SuppressWarnings("unchecked") public void forEachRemaining(Consumer<? super E> consumer) { Objects.requireNonNull(consumer); final int size = ArrayList.this.size; int i = cursor; if (i >= size) { return; } final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } while (i != size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); } // update once at end of iteration to reduce heap write traffic cursor = i; lastRet = i - 1; checkForComodification(); } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
public Iterator<E> iterator() {
return new Itr();
}
当我们调用集合的iterator()方法时会创建一个内部类Itr对象返回。
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;
}
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
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
}
集合的remove()方法,不管调用哪个都会改变modCount的值
但是如果通过iterator对象的remove()方法来移除元素时,它实质上调用的也是集合的remove()方法,但是它移除完了之后会更新Itr对象的expectedModCount的值,所以就保证了expectedModCount和最新的modCount相等,所以就不会抛出异常
LinkedList
- LinkedList底层实现是双向链表
- 特点:增删快,空间理论上无限,可以存储重复的元素
- LinkedList集合的方法和ArrayList集合是差不多的,但是底层没有索引,之所以能够通过get(int index)去获取指定位置的值是因为LinkedList是连续的
- add(E e)采用的是尾插法
- LinkedList可以存储null
HashMap
-
HashMap的底层实现是:哈希表+单链表+红黑树(jdk8包括8之后,在8之前是哈希表+单链表)
-
当通过HashMap的无参构造创建map集合时,它的数组为空,当我们进行第一次put元素时,会进行扩容,容量变为16,往后如果再进行扩容,每次扩容的大小为原来容量大小的两倍
-
HashMap中的键和值允许为null,HashTable中则不允许,当put()的键值对中的键已经存在,则会将原来的值进行覆盖
-
HashMap集合不能在foreach中通过keySet进行遍历使用集合的remove()方法对集合元素进行移除,会报并发修改异常,原因和ArrayList集合一样
-
HashMap每次扩容的要求是table数组的元素数量大于阈值,threshold(阈值)属性是用于记录下一次扩容时数组中元素个数
-
增加元素:扩容机制和转红黑树条件,当链表长度大于8时才调用转红黑树的方法,但是调用了转红黑数的方法中会先判断节点数组长度(容量)是否大于64,不大于则进行扩容,大于则进行转红黑树
-
HashMap的添加元素方法putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
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)))) //计算到的位置上不为空则对该位置上的key值进行判断是否和新添加的Node上的哈希值和key相等,如果相等则将该位置上的节点保存,为后面的覆盖节点值操作做准备 e = p; //将该位置上的Node节点取出,为后面的覆盖value值做准备 //如果计算得到位置上的节点的key值不同,则需要把新添加的节点放到该节点后面,所以需要进行判断该节点是树结点还是链表节点 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); //在添加完了节点后,判断链表的长度是否达到了转化为红黑树的要求,链表长度达到了8 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //计算到的位置上的key值进行判断是否和新添加的Node上的哈希值和key相等,直接跳出循环到后面的覆盖节点值操作,判断相等的依据是两个对象是否指向同一块内存地址或者它们通过equals()比较 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //覆盖原有相同key的Node节点的值 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的底层实现JDK1.8是一个 可变的哈希表+链表或者 数组+红黑树,JDK1.8前是数组+链表 ,哈希表是一个 Node<K,V>[]数组,Node<K,V>这个数组在创建集合对象初始时为null,在添加第一个元素时会进行扩容,将扩容后的数组重新赋值给table,这个table的容量为初始的16
transient Node<K,V>[] table;
-
remove()方法
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; //判断节点数组是否为null && 数组长度不为0 && 通过key通过hash算法算出数组的位置不为null if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; //通过hash算法计算key位置上的元素的hash和需要移除元素的hash值相等 && (计算出来数组的索引上的元素的key和需要移除的元素的key相等(有可 能key为引用) || 这两个key值相等) //满足上述条件则说明数组上的元素就是需要移除的元素 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; //如果数组上的元素不是需要移除的目标元素,则判断这个元素后面还有没有节点,如果有则继续查找,没有则返回null else if ((e = p.next) != null) { //判断数组上的这个元素是不是红黑树的节点类型,如果是则将数组上的元素强转为红黑树节点类型,并通过调用getTreeNode()方法获取这个节点 if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { //如果不是树结点类型,就是链表类型,直接遍历这个链表进行查找,如果没有找到符合条件的节点,最终会返回null do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } //如果在上面代码中找到了符合条件的节点,进行移除操作 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { //如果找到的节点是红黑树结点类型,则调用红黑树里面的移除节点方法 if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); //如果是哈希表上的元素(单链表的头结点),则将数组上面的元素的下一个直接覆盖到数组上面来 else if (node == p) tab[index] = node.next; //否则就是链表上面的,直接修改指针的指向 else p.next = node.next; //记录对集合的操作次数 ++modCount; //把集合内的元素减一 --size; afterNodeRemoval(node); //返回被删除的节点 return node; } } //如果上面的代码没有找到符合条件的节点,则删除不了,结果返回null return null; } //通过红黑树的移除节点的方法 final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) { //map代表的是需要移除节点的集合对象,tab是需要删除节点的集合的数组,movable代表只要值相等就移除 int n; if (tab == null || (n = tab.length) == 0) return; int index = (n - 1) & hash; //hash是需要删除的节点的hash,通过这个节点的hash值和数组的长度进行运算的到索引值 TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl; TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev; if (pred == null) tab[index] = first = succ; else pred.next = succ; if (succ != null) succ.prev = pred; if (first == null) return; if (root.parent != null) root = root.root(); if (root == null || root.right == null || (rl = root.left) == null || rl.left == null) { tab[index] = first.untreeify(map); // too small return; } TreeNode<K,V> p = this, pl = left, pr = right, replacement; if (pl != null && pr != null) { TreeNode<K,V> s = pr, sl; while ((sl = s.left) != null) // find successor s = sl; boolean c = s.red; s.red = p.red; p.red = c; // swap colors TreeNode<K,V> sr = s.right; TreeNode<K,V> pp = p.parent; if (s == pr) { // p was s's direct parent p.parent = s; s.right = p; } else { TreeNode<K,V> sp = s.parent; if ((p.parent = sp) != null) { if (s == sp.left) sp.left = p; else sp.right = p; } if ((s.right = pr) != null) pr.parent = s; } p.left = null; if ((p.right = sr) != null) sr.parent = p; if ((s.left = pl) != null) pl.parent = s; if ((s.parent = pp) == null) root = s; else if (p == pp.left) pp.left = s; else pp.right = s; if (sr != null) replacement = sr; else replacement = p; } else if (pl != null) replacement = pl; else if (pr != null) replacement = pr; else replacement = p; if (replacement != p) { TreeNode<K,V> pp = replacement.parent = p.parent; if (pp == null) root = replacement; else if (p == pp.left) pp.left = replacement; else pp.right = replacement; p.left = p.right = p.parent = null; } TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement); if (replacement == p) { // detach TreeNode<K,V> pp = p.parent; p.parent = null; if (pp != null) { if (p == pp.left) pp.left = null; else if (p == pp.right) pp.right = null; } } if (movable) moveRootToFront(tab, r); }
-
HashMap集合的扩容方法
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; //先判断原来的数组是不是没有元素,从而记录原来数组的容量 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { //判断原来容量是否已经达到了最大值,如果达到了则不再进行扩容,将下一次扩容的标准设为Integer的最大值 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; }else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)//新的容量扩容为原来的两倍,扩容的要求是原来的容量必须大于默认的容量16才能进行扩容 newThr = oldThr << 1; // double threshold 下一次扩容的阈值也是原来的双倍 }else if (oldThr > 0) // initial capacity was placed in threshold 当我们通过给定容量和加载因子的构造方法创建集合时才会进入这里 newCap = oldThr; //将阈值当作新容量 这个阈值是创建集合对象时传入的初始容量的2次幂 else { // zero initial threshold signifies using defaults 通过无参构造创建的集合对象第一次扩容将会进入这里 newCap = DEFAULT_INITIAL_CAPACITY; //新的容量将是默认的初始容量16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //阈值则是加载因子*默认的初始容量 16*0.75 = 12 下一次当table中的元素个数达到了12个则再次进行扩容 } 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<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //创建扩容后的新数组,但是里面没有元素,还需要把原来数组的元素移过来 table = newTab; //将旧数组中的元素放到新数组中 if (oldTab != null) { //循环遍历旧数组的所有元素 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //如果第j个元素不为空 if ((e = oldTab[j]) != null) { oldTab[j] = null; //如果第j个元素后面没有元素了,在新数组中计算位置,把第j个元素放到新数组中计算到的位置 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //如果第j个元素后面还有节点,判断数组上的第j个元素是否为树节点 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order 如果不是树节点,则是链表节点,通过循环将链表节点依次重新连接,并把头结点放进新数组中 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; 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); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } //如果最后一个节点不为空,则把最后一个节点的下一个指向到空 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; //把头结点放进新数组中 } } } } } return newTab; }
-
由单链表转红黑树方法(实际上还没转成红黑树,只是把单链表的节点转成了红黑树的节点)
//当单链表的的节点个数大于等于8个时,将单链表转成红黑树,treeifyBin()只是将单链表的节点转成红黑树的节点 final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) //先判断哈希表的长度是否达到了64,达到了才进行转红黑树,没达到就进行扩容 resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { //根据hash值计算的索引位置上不为空进行转红黑树 TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); //将链表的节点转成红黑树的节点 if (tl == null) hd = p; //将链表中的第一个节点作为头节点,只有循环第一次能进去 else { p.prev = tl; //将新转成的红黑树节点的前驱节点指向上一个节点 tl.next = p; //将上一个节点的下一个节点指向新转成的红黑树节点 } tl = p; //将p节点作为下一次循环的上一个节点 } while ((e = e.next) != null); //取单链表的下一个节点 if ((tab[index] = hd) != null) //判断根据hash值计算到的索引位置是否为null,不为null则进行转红黑树 hd.treeify(tab); //进行转红黑树 } } //转换成红黑树的节点 TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { return new TreeNode<>(p.hash, p.key, p.value, next); } //转成红黑树的方法 final void treeify(Node<K,V>[] tab) { TreeNode<K,V> root = null; for (TreeNode<K,V> x = this, next; x != null; x = next) { next = (TreeNode<K,V>)x.next; x.left = x.right = null; if (root == null) { x.parent = null; x.red = false; root = x; } else { K k = x.key; int h = x.hash; Class<?> kc = null; for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key; if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1; else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) dir = tieBreakOrder(k, pk); TreeNode<K,V> xp = p; if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x; root = balanceInsertion(root, x); break; } } } } moveRootToFront(tab, root); }
-
HashMap的toString()是调用的AbstractMap的toString(),实际上调用的是迭代器方法
public String toString() { Iterator<Entry<K,V>> i = entrySet().iterator(); if (! i.hasNext()) return "{}"; StringBuilder sb = new StringBuilder(); sb.append('{'); for (;;) { Entry<K,V> e = i.next(); K key = e.getKey(); V value = e.getValue(); sb.append(key == this ? "(this Map)" : key); sb.append('='); sb.append(value == this ? "(this Map)" : value); if (! i.hasNext()) return sb.append('}').toString(); sb.append(',').append(' '); } }
Hashtable
- 底层是一个哈希表
- 默认初始容量是11(一创建对象就有容量了,不需要在第一次添加时扩容才有),如果通过传递容量的构造方法传递一个0创建对象则容量是1,默认加载因子是0.75
- 线程安全的集合
- key和value都不能存放null
- 如果存放的key已经存在,则会将对应的value进行覆盖
- 每次扩容为原来的两倍+1
TreeSet
-
TreeSet的底层实现是TreeMap集合(红黑树)
-
TreeSet不可以存放相同元素(是否相同取决于比较方法如何实现,Comparable接口的compareTo(),Comparator的compara())
-
TreeSet存取顺序是无序的,不可以存放null
-
在存放自定义的对象时,存放的对象的类需要实现Comparable接口或者是在创建TreeSet集合时传递一个Comparator接口的实现类对象
-
TreeSet存进去的元素在内部是有序的
-
在TreeSet中,判断一个对象是否存在集合中是通过比较器或者自然排序的排序方法进行比较,如果比较规则中使用到了equals()则关注点在equals()
final Entry<K,V> getEntry(Object key) { //key为需要判断是否存在集合中的对象 // Offload comparator-based version for sake of performance if (comparator != null) //判断是否有外部比较器,如果有外部比较器优先用外部比较器去寻找 return getEntryUsingComparator(key); if (key == null) //如果需要查找的对象是null报空指针异常 throw new NullPointerException(); @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; //将key进行强转 Entry<K,V> p = root; //从根节点开始进行查找 while (p != null) { int cmp = k.compareTo(p.key); //根据比较规则去选择继续查找的方向,是在左子树还是右子树 if (cmp < 0) p = p.left; //往左子树方向去找 else if (cmp > 0) p = p.right; //往右子树去找 else return p; //当比较方法返回0说明找到了相同的对象,直接返回节点 } return null; //如果遍历完了红黑树都没找到返回null } final Entry<K,V> getEntryUsingComparator(Object key) { //通过外部比较器进行查找节点位置 @SuppressWarnings("unchecked") K k = (K) key; //将需要查找的对象转成红黑树中存放的节点类型 Comparator<? super K> cpr = comparator; if (cpr != null) { Entry<K,V> p = root; //从根节点开始查找 while (p != null) { int cmp = cpr.compare(k, p.key); if (cmp < 0) p = p.left; else if (cmp > 0) p = p.right; else return p; } } return null; }
-
当TreeSet集合同时实现自然排序和比较器时,比较器会优先于自然排序(当两个比较器共存时会优先使用Comparator比较器)
-
当TreeSet调用了add(E e)实际上是调用的是TreeMap的put(K key, V value)方法
public V put(K key, V value) {
//root是用于保存根节点的一个属性
Entry<K,V> t = root;
if (t == null) { //先判断根节点是否为空(实际是判断集合是否为空)
compare(key, key); //作用是判断存储的对象是否实现了自然排序或者集合中是否有比较器,如果都没有则会报异常
root = new Entry<>(key, value, null); //创建一个节点作为根节点
size = 1; //集合元素+1
modCount++; //对集合操作+1
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator; //comparator是集合的外部比较器
if (cpr != null) { //如果没有外部比较器就使用存储对象中的比较规则
do { //通过循环找到合适的位置
parent = t; //保存节点,作用是为了后面在添加新节点时新节点的父节点
cmp = cpr.compare(key, t.key); //根据规则进行寻找准备添加的元素的位置
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else //如果返回值是0则说明集合中已经存在该元素,不需要再进行添加
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
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);
}
Entry<K,V> e = new Entry<>(key, value, parent); //创建一个新节点,它的父节点就是上面保存的parent节点
if (cmp < 0) //判断它是在父节点的左边还是右边,让父节点指向新添加的节点
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e); //红黑树进行平衡
size++;
modCount++;
return null;
}
TreeMap
- 底层是红黑树
- key不能为null但是value可以为null
- 当key是自定义对象时,自定义对象需要实现Comparable接口或者在创建TreeMap集合对象时传递一个比较器
- 当存储的key在自然排序或者比较器的排序方法中返回0时则会替换对应key的value值
- 当TreeSet集合同时实现自然排序和比较器时,比较器会优先于自然排序
Queue
-
Queue是一个接口,我们一般使用他的实现类LinkedList集合
-
特点:先进先出、用完即删
//因为使用的是LinkedList所以采用的是双向链表结构,poll()返回头结点的值并把头节点移除 public E poll() { final Node<E> f = first; return (f == null) ? null : unlinkFirst(f); //判断头结点是否为空,为空说明链表中没有元素,返回null,否则调用方法删除头结点并返回头结点的值 } //移除头结点方法 private E unlinkFirst(Node<E> f) { // assert f == first && f != null; final E element = f.item; //先把头结点的值保存下来,用于返回 final Node<E> next = f.next; //记录头结点的下一个节点 f.item = null; //把头结点的数据设为null f.next = null; // help GC 让头结点不再指向其他节点,方便GC回收 first = next; //让原来的头结点的下一个节点作为新的头结点 if (next == null) //判断头指针指向的是不是为null,如果是说明原本链表中只有一个元素,被移除了所以链表中没有了元素,所以头指针和尾指针都要指向null,所以就修改尾指针的指向为null last = null; else //否则的话就把新的头结点的前驱指针指向null next.prev = null; size--; //集合元素-1 modCount++; //对集合的操作+1 return element; //返回被移除的节点的值 }
Stack
- 后进先出,用完即删
//删除栈顶,也就是最后一个加入进去的对象,并返回被删除对象,Stack继承自Vector
public synchronized E pop() {
E obj;
int len = size(); //size()作用是得到数组的元素个数
obj = peek(); //peek()返回数组中最后一个对象
removeElementAt(len - 1);
return obj;
}
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) { //如果需要删除的对象后面没有对象了则j的值为0,如果大于0,则说明还有j个对象,需要往前面移动
System.arraycopy(elementData, index + 1, elementData, index, j); //把后面的j个对象往前面挪一位
}
elementCount--; //元素个数-1
elementData[elementCount] = null; /* to let gc do its work */ //把需要最后一个位置赋值为null让jvm去回收它
}
Iterator迭代器
-
Iterator是一个接口,有四个方法,hasNext()、next()、remove()、forEachRemaining(Consumer<? super E> action)
-
迭代器用于遍历集合
-
迭代器服务于Collection接口或者以下的集合
序列化
- 是一个接口,接口里面没有任何方法,接扣名:Serializable
- 序列化的作用:将一个对象的状态持久化保存,可以进行网络传输
- 序列化的目的就是为了反序列化
- 反序列化:将一个对象的状态进行还原(创建对象)
- 序列化方法:将需要序列化的类实现Serializable接口,通过ObjectOutputStream类的对象将需要序列化的对象写出
- 不能被序列化:
- private transient修饰的属性
- private static修饰的属性或者方法
- 实现该接口的类会有一个序列号,序列号用于在进行序列化和反序列化时进行加密解密
- 如果改变类的结构有可能会导致序列号发生改变,从而导致在反序列化时出现异常
- 解决办法:
- 让新添加的内容不被序列化
- 使用transient关键字
- 手动赋值序列号,使类的序列号和被序列化时的序列号一致
- 解决办法:
- transient关键字不能够修饰方法
- 如果子类继承了父类,父类实现了序列化接口,则子类不需要实现序列化接口也能被序列化
创建对象的方式
- 通过new关键字
- 克隆
- 需要实现Cloneable接口,这个接口中什么都没有,只是起标志作用(标志接口)
- 不会调用构造方法
- 反序列化
- 不会调用构造方法
- 反射
单例实现方式
- 懒汉式
- 饿汉式
- 枚举
- 枚举实现单例的好处:防止序列化攻击、反射攻击
泛型
-
泛型只在编译时有效,当运行时JVM中是没有泛型这一说法的,这就是泛型的 可擦除机制
-
泛型是在编译时起一个约束作用,如:
public class Test { public static void main(String[] args) { List<String> list=new ArrayList<>(); } }
上面定义的ArrayList集合中只能存放String类型的对象,如果放别的对象会编译不通过
-
在JDK1.7前泛型只能List list=new ArrayList();这样定义,但是在JDK1.8后有了类型推断,所以我们可以省略后面的泛型不写,也就是List list=new ArrayList<>();
-
如果我们在使用一个泛型类或者泛型接口时不去确定它的泛型类型,那么它默认是Object类型
-
泛型的优点:
- 类型安全。 泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。
- 消除强制类型转换。 泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
- 潜在的性能收益。 泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。
-
通配符的缺点
public static void fun(List<?> list) {…}
带有通配符的参数不能使用与泛型相关的方法,例如:list.add(“hello”)编译不通过。List<?> list参数中的通配符可以被赋任何值,但同时你也不知道通配符被赋了什么值。当你不知道“?”是什么时,会使你不能使用任何与泛型相关的方法。也就是说fun()方法的参数list不能再使用它的与泛型相关的方法了。例如:list.add(“hello”)是错误的,因为List类的add()方法的参数是T类型,而现在你不知道T是什么类型,你怎么去添加String的东西给list呢?如果使用者在调用fun()方法时传递的不是List< String>,而是List< Integer>时,你添加String当然是不可以的。当然,还可以调用list的get()方法。就算你不知道“?”是什么类型,但它肯定是Object类型的。所以你可以:Object o = list.get(0);
-
泛型上限:例如List<? super String> 表示List集合中只能放类型为String或者String父类的对象
-
泛型下限:例如List<? extends String>表示List集合中只能放类型为Sring或者String子类的对象
-
泛型通配符:?
- 方法参数带有通配符会更加通用;
- 带有通配符类型的对象,被限制了与泛型相关方法的使用;
- 下边界通配符:可以使用返回值为泛型变量的方法;
- 上边界通配符:可以使用参数为泛型变量的方法。
-
泛型类、接口的使用
当我们定义了一个泛型的接口或者泛型的类,如果有一个子类/接口去继承它,这个类/接口需不需要去确定这个父类的泛型呢?
public interface Animal <E>{ } public interface Person<E> extends Animal<E>{ } //实现含有泛型的接口时,不确定它的类型,并让这个类也成为一个泛型类 public class Student <E> implements Person<E>{ } //实现一个泛型接口,确定它的类型 public class Student implements Person<String>{ }
当一个接口继承一个接口时,我们可以让这个父接口继续使用泛型,也可以确定这个父接口的类型。
当一个类实现一个泛型接口,我们确定接口的泛型,也可以继续使用泛型,还可以让我们这个类使用泛型
-
泛型可以用在接口、方法、属性、类上
//泛型用在接口上(泛型接口) public interface Person <E>{ } //泛型用在类上(泛型类) public class Student <E>{ } //泛型用在方法上(泛型方法) public <E> void test() { } //用泛型修饰属性 public E name;
需要注意的是:
public class Student <E>{ //泛型方法 public <E> void test(E name) { System.out.println(name); } //使用了泛型的方法 public void t(E name) { System.out.println(name); } } //测试 public class Test { public static void main(String[] args) { Student<String> s = new Student<>(); s.test(123); //编译通过,运行正常 s.t(123); //编译不通过 } }
上面这两个方法看似都是泛型方法,其实不是,只有test()方法才是泛型方法,泛型方法的定义是在访问权限修饰符和返回值之间需要定义一个泛型,而t()方法只是用了这个类的泛型的一个普通方法
从我们的测试中就可以知道,t()方法的参数类型是和创建对象时确定的泛型类型一致,而test()方法是在调用方法时传的参数时确定该方法参数的类型,这就是两者的区别。
IO
File
- File类对象是文件和目录路径名的抽象表示形式
- 常用方法:
- mkdir()创建一个目录
- mkdirs()如果父级目录不存在也会去创建
- exists()判断目录或者文件是否存在
- createNewFile()当且仅当不存在具有此抽象路径名指定名称的文件时,创建一个指定名称的文件
InputStream
- 抽象类
- 是所有字节输入流的基类
OutputStream
- 抽象类
- 是所有字节输出流的基类
Reader
- 抽象类
- 是所有字符输入流的基类
Writer
- 抽象类
- 是所有字符输出流的基类
FileInputStream
- FileInputStream是文件字节输入流,操作的单位是字节(可以操作所有二进制文件),作用是从文件中读取数据到程序中
FileOutputStream
- FileOutputStream是文件字节输出流,操作的单位是字节(可以操作所有二进制文件),作用是将程序中的数据写出到文件中
- 如果通过构造方法传入的抽象路径中的文件不存在时,会自动创建一个对应名称的文件,但是如果目录不存在则会报异常
- 在构建一个FileOutputStream对象时可以通过构造方法传入一个boolean值去决定是否在创建一个FileOutputStream对象时清空抽象路径对应的文件中的内容,默认为false,如果为true则不清空
FileWriter
- 文件字符输出流
- 单位是字符,用于写出文本内容
FileReader
- 文件字符输入流
- 单位是字符,用于读取文本文件
BufferedOutputStream
- 默认缓冲区大小是8192个字节,最大为Integer.MAX_VALUE
- 当写出数据时,数据会先保存到缓冲区中,当缓冲区满了、调用close()关闭流、调用flush()刷新时会将缓冲区的数据写到目的源中
- 如果通过write(byte[] b,int off,int len)写出数据时,如果len大于缓冲区的长度则会直接通过字节流写出,而不经过缓冲区
public synchronized void write(byte b[], int off, int len) throws IOException {
if (len >= buf.length) {
//如果需要写出的数据长度超过或者等于缓冲区的大小,则先将缓冲区的数据写出去,然后直接通过字节流直接写出去,而不用经过缓冲区
flushBuffer();
out.write(b, off, len);
return;
}
if (len > buf.length - count) {//如果需要写出的数据的长度不超过缓冲区大小,但是超过了缓冲区剩余空间,则先把缓冲区中的内容写出去
flushBuffer();
}
System.arraycopy(b, off, buf, count, len); //将需要写出去数据保存到缓冲区中
count += len; //更改缓冲区中已经存在的数据数量
}
BufferedInputStream
-
默认缓冲区大小是8192个字节,最大为Integer.MAX_VALUE
-
count代表的是缓冲区中元素的个数(byte数组中元素的个数)
-
pos代表下一次需要读的数据的在数组中的索引
-
buf是缓冲区中存放数据的数组
-
如果扩容后的容量大于marklimit的话,缓冲区每次扩容为原来的两倍
-
marklimit为最大提前读取量
-
markpos为标记的位置
-
缓冲字节输入流是先通过字节流的read()把数据读到缓冲区中,等到需要取数据的时候再从缓冲区中拿
//读取一个字节数据的方法,返回的是对应数据的ascii码值 public synchronized int read() throws IOException { if (pos >= count) {//如果上一次读到的数据已经是最后一个元素,则这次需要通过fill()去重新取数据填充到buf数组中 fill(); if (pos >= count) //如果填充之后这次需要读取的数据还是最后一个,说明后面没有数据了,直接返回-1 return -1; } return getBufIfOpen()[pos++] & 0xff; //返回buf数组中索引为pos的数据 } //对buf数组进行填充的方法 private void fill() throws IOException { byte[] buffer = getBufIfOpen(); //获取存放数据的数组buf if (markpos < 0) //判断有没有做标记,如果有则需要考虑保存标记位置到目前读的位置之间的内容,没有标记直接从0开始填充新数据 pos = 0; /* no mark: throw away the buffer */ else if (pos >= buffer.length) /* no room left in buffer */ //如果已经读到缓冲数组的最后面了,需要把pos指针重新定位 if (markpos > 0) { /* can throw away early part of the buffer 把mark做标记的数据往前面移动保存,然后方便后面读新内容到缓冲数组中 */ int sz = pos - markpos; //sz为标记位置到目前读到的位置中间需要保存的数据的长度 System.arraycopy(buffer, markpos, buffer, 0, sz); //把需要保存的内容移到数组的开始位置到sz位置 pos = sz; //把读内容的指针定位到sz位置 markpos = 0; //标记位置的指针移到数据移动后的开头位置,也就是数组索引为0这个位置 } else if (buffer.length >= marklimit) { //如果缓冲区数组的长度大于限制最大的读取量则标记无效 markpos = -1; /* buffer got too big, invalidate mark */ pos = 0; /* drop buffer contents */ //从0开始填充新数据 } else if (buffer.length >= MAX_BUFFER_SIZE) {//如果缓冲区数组长度已经大于等于缓冲区最大空间,则报内存不足错误 throw new OutOfMemoryError("Required array size too large"); } else { /* grow buffer */ //如果需要保存的标记内容为从0到pos,如果先要填充新数据,则需要扩充缓冲区数组长度 int nsz = (pos <= MAX_BUFFER_SIZE - pos) ? //如果在缓冲区范围内,则进行原来的两倍扩容,如果不在,直接给最大值 pos * 2 : MAX_BUFFER_SIZE; if (nsz > marklimit) //判断新容量是否超过了设定的最大读取容量,如果超过了,则用设定的值作为新容量 nsz = marklimit; byte nbuf[] = new byte[nsz]; //创建一个新byte数组作为新的缓冲区数组 System.arraycopy(buffer, 0, nbuf, 0, pos); //把旧的缓冲区数组中的内容复制到新的缓冲区数组中 if (!bufUpdater.compareAndSet(this, buffer, nbuf)) { throw new IOException("Stream closed"); } buffer = nbuf; //把新的缓冲区数组赋值给旧的缓冲区数组,扩容完毕 } count = pos; //数组中元素数量为pos个 int n = getInIfOpen().read(buffer, pos, buffer.length - pos); //读取新的内容到缓冲区数组中,从上次读到的pos位置开始存放,读取缓冲区数组长度-pos这个位置,也就是剩余能用的空间 if (n > 0) //如果没有更多内容可以读n会返回为-1,否则返回读取到长度,数组中现有的元素数量要加上新读到的数量 count = n + pos; }
BufferedWriter
- 带缓冲区的字符输出流
newLine()
写出一行分隔符(换行)- 默认缓冲区大小8192字节
BufferedReader
- 带缓冲区的字符输入流
readLine()
读取一行,如果没有更多内容,返回null- 默认缓冲区大小8192字节
InputStreamReader
-
输入转换流,是字节流通向字符流的桥梁
-
本质是字符流,可以通过转换流读取不同编码的文件
-
我们可以在创建InputStreamReader对象时在构造方法中传递一个编码格式,以我们设定的编码把数据读进来
//转换流的一个构造方法 public InputStreamReader(InputStream in, String charsetName) throws UnsupportedEncodingException{ super(in); if (charsetName == null) throw new NullPointerException("charsetName"); sd = StreamDecoder.forInputStreamReader(in, this, charsetName); } //经过转换流把一个文件字节输入流转成一个带缓冲区的字符输入流并以GBK的编码读取的对象 @Test public void test(){ BufferReader br = new BufferReader(new InputStreamReader(new FileInputStream("test.txt"),"GBK")); }
OutputStreamWriter
-
输出转换流,是字符流通向字节流的桥梁
-
本质是字符流,可以通过转换流写出不同编码的内容
-
当传入的OutputStream对象没有设置需要拼接,那么目的源如果已经存在并且里面有内容并且编码与我们需要写出去时设置的编码不同,那么当我们写出数据时,目的源的编码会变成我们设置的编码
@Test public void test02(){ BufferedWriter bw = null; try { //原本目的源编码为GBK,当我们把数据写出去时,目的源的编码变成了UTF-8 bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("E:\\File\\a.txt"), "UTF-8")); bw.write("呵呵呵呵"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { IOUtils.closeAll(bw); } }
ObjectInputStream
- 对象输入流,用来读取对象的流,读取对象的操作为反序列化
ObjectOutputStream
- 对象输出流,用来写出对象的流,写出对象的操作为序列化
PrintStream
- 打印字节流
- 有自动刷新机制,自动调用flush()
- 只能输出,不能输入
- 原样打印内容,如调用print(97),打印到目的源上的内容就是97,而不是a
- System.out就是PrintStream的一个对象,可以通过System.setOut(PrintStream out)和System.setInt(InputStream in)来重定向输出和输入的目的源和目标源
- 案例:改变System.out的目的源和System.in的目标源
//改变System.out的目的源
@Test
public void test01(){
PrintStream ps = null;
try {
ps = new PrintStream("IO1.txt");
System.setOut(ps);
System.out.print("hello,world");
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
if (ps != null){
ps.close();
}
}
}
//改变System.in的目标源
@Test
public void test02(){
FileInputStream fis = null;
try {
fis = new FileInputStream("IO1.txt");
System.setIn(fis);
Scanner sc = new Scanner(System.in);
System.out.println(sc.nextLine());
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
if (fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
PrintWriter
- 打印字符流
- 只能输出,不能输入
ByteArrayInputStream
- 内存输入流(字节流)
- 内存->程序
- 特点:操作数据量小,效率高
- 无法关闭
ByteArrayOutputStream
- 内存输出流(字节流)
- 程序->内存
- 特点:操作数据量小,效率高
- 无法关闭
- 默认为32个字节
- 最大Integer.MAX_VALUE-8
RandomAccessFile
- 随机访问流(字节流)
- 特点:
- 当目标源文件中存在内容时会从偏移量位置开始进行覆盖原有内容
- 有指针,可以根据指针的指向开始读或者写
- 实例化对象时,偏移量(指针)为0
- 可以根据实例化对象时通过构造方法传递的参数去确定这个对象是否能对文件进行读写操作
- 案例:断点续传
- 当进行传输文件时,如果没传输完时程序终止了,再次启动程序时可以接着上次传输的位置继续传输文件内容(因为有指针)
public class Test{
public static void main(String[] args){
RandomAccessFile r = null;
RandomAccessFile rw = null;
try {
r = new RandomAccessFile("E:\\File\\1.mp4", "r");//第二个参数传递"r"代表只读
rw = new RandomAccessFile("E:\\File\\2.mp4", "rw");//第二个参数传递"rw"代表读写
r.seek(rw.length());//seek()设置指针的位置,也就是偏移量
rw.seek(rw.length());//seek()设置指针的位置,也就是偏移量
byte[] bs = new byte[10];
int len;
while((len = r.read(bs)) != -1){
rw.write(bs,0,len);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeAll(r,rw);
}
}
}
//自定义关闭流的工具类
public class IOUtils {
public static void closeAll(Closeable ...closeables){
for (Closeable closeable : closeables) {
if (closeable != null){
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
总结
多线程
- 进程:操作系统资源基本分配的基本单位
- 线程:处理器任务调度和执行的基本单位
实现多线程的方式
- 自定义线程类继承Thread类,重写run()方法,实例化自定义线程类对象并通过start()启动线程,使线程进入就绪状态,等待cpu调度
- 自定义任务类实现Runnable接口,重写run()方法,实例化任务类对象,实例化Thread对象,通过Thread的构造方法传递任务类对象,通过Thread类对象调用start()启动线程,使线程进入就绪状态,等待cpu调度
- 自定义任务类实现Callable接口,重写call()方法,call()有返回值和可以抛出异常
- 线程池
Thread
-
Thread为线程类,其本质也是实现了Runnable接口
-
可以通过setPriority(int newPriority)设置线程的优先级,线程被创建出来的时候优先级是5,最大为10,最小为1,线程优先级越小被CPU选中的几率越高
-
sleep(long millis)和wait()都可以使线程进入阻塞状态,sleep(long millis)是Thread类的静态方法,它的作用是使线程进入休眠状态指定毫秒数,时间到了会自动解除阻塞状态为就绪状态等待CPU的调度,并且在休眠期间是不会释放资源的,就是不会释放cpu的使用权,而wai()方法是Object类的方法,它的作用是使线程进入等待队列中,它需要手动调用notify()或者notifyAll()来唤醒线程,在等待期间它是释放资源的,wait()由同步监视器调用
-
join()是先让调用这个方法的线程先执行完
-
yield()是使当前线程放弃资源重新回到就绪状态竞争cpu,但是它只能给和它优先级相同或者比它优先级小的线程礼让,yeild()不会释放资源
-
案例:测试yeild()不释放资源
package com.qf.xjw.test; public class Demo02 { public static void main(String[] args) { MyThread myThread = new MyThread(); Thread thread1 = new Thread(myThread,"A"); Thread thread2 = new Thread(myThread,"B"); Thread thread3 = new Thread(myThread,"C"); thread1.start(); thread2.start(); thread3.start(); } } class MyThread implements Runnable{ private int num = 0; @Override public void run() { synchronized (this){ while(true){ if (num < 100){ num++; if ("A".equals(Thread.currentThread().getName())){ Thread.yield(); } System.out.println(Thread.currentThread().getName()); } } } } }
打印结果是当线程A先拿到锁进入同步块,调用yeild()应该让其他线程执行,但是线程名为A的一直打印,如果yeild()释放资源那么它们将不会一直打印
Runnable
- 自定义类实现Runnable接口并重写run()则这个类就是一个任务类,在实例化Thread对象时通过构造方法传递一个任务类对象,则这个线程对象就会执行任务类中run()
- 继承Thread和实现Runnable接口都可以实现多线程,但是实现Runnable()更好,因为在java中类是可以实现多个接口,但是只能直接继承一个类,实现Runnable()接口我们的线程的扩展性更好
线程的状态
- 线程的状态有新生、就绪、运行、阻塞、消亡
同步锁(synchronized )
-
同步代码块
- 多个线程的同步监视器需要为同一个对象
- 同步对象的引用不要发生改变,可以改变对象属性内容
- 案例:手机销售
public class Demo01 { public static void main(String[] args) { for (int i = 1; i <= 10; i++) { new Store("第"+i+"家华为专卖店").start(); } } } class Store extends Thread{ private static Integer num = 0; private static Object obj = new Object(); public Store(){ } public Store(String name){ super(name); } @Override public void run() { sell(); } private void sell(){ while(true){ //如果锁的是num,在后面我们会对num进行++操作,每次++之后的num都是一个新对象,这样的话会锁不住对象,所以我们应该锁obj这个对象 //即使obj的属性被改变,只要obj的引用没发生改变就能被锁住 synchronized (num){ if (num < 1000){ System.out.println(Thread.currentThread().getName() +"卖出了第"+(++num)+"台P50手机"); }else{ System.out.println(Thread.currentThread().getName() +"P50售罄"); break; } } try { sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } }
-
同步方法
- 当一个线程执行了同步方法,其他线程需要等待
- 多个线程调用的同步方法必须是同一个方法(同步方法为静态方法或者是同一个对象的方法)
- 同步方法的同步监视器为当前方法
Lock
- Lock是一个接口,使用它的实现类ReentrantLock
- ReentrantLock是一个类,需要通过对象显式的获取锁和释放锁
- ReentrantLock是可重入锁,对象互斥锁,可以通过构造方法传递参数设置为公平锁,默认为非公平锁
- 公平锁:根据等待顺序去获得锁,先等待的先得到锁
- 与synchronized相比不容易发生死锁,因为可以显式的去释放锁,而synchronized是隐式释放
线程池
-
线程池的有点是可以对线程进行重复利用
-
通过线程池的工具类Executors创建线程池
-
newSingleThreadExecutor()创建一个单例线程来完成提交的任务,线程池中只有一个线程完成提交的任务
-
newFixedThreadPool(int nThreads)根据传递的参数在线程池中去创建指定个数的线程
-
newCachedThreadPool()创建缓冲线程池,根据任务量来创建线程,在创建线程去执行任务时,先创建的线程完成了当前任务时如果还有任务会继续去执行其他任务,当一个线程在60s内没有任务会自动销毁
-
包装类
Integer
-
在底层实现中,Integer类中存在一个IntegerCache静态内部类,这个内部类中存在一个类型为Integer类型的静态的数组,这个数组里存放着我们常用的一些数字,最小固定为-128,最大为Integer.MAX_VALUE -129,这个最大值可以在JVM虚拟机的设置中设定,默认是127
private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; //默认为127 String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} }
-
在自动装箱时调用的是valueOf(int i)方法进行自动装箱,而这个方法会对需要转换的int数字进行一个范围判断,判断这个数字是否在缓冲数组中,如果在则直接返回数组中对应的Integer对象,否则需要重新new。
public static Integer valueOf(int i) { //进行范围的判断,如果在该数组的范围内,则返回数组中对应位置的对象,不在该范围则new一个对象 if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
-
当一个Integer对象和一个int类型的数字通过==进行比较时,比较的是他们的值,因为Integer对象会通过intValue()方法进行拆箱成int类型的数字再进行比较,可以通过反编译下面这个文件的生成的字节码论证
public class Demo06 { public static void main(String[] args) { Integer integer = new Integer(39); System.out.println(integer == 39); } }
String
-
String是不可变的,因为String的底层是一个被final修饰字符数组(jdk9之前,9包括9之后是一个byte数组)
案例分析
public class Test{ public static void main(String args[]) { String s1 = "ab"; String s2 = "abc"; String s3 = s1 + "c"; //s3 == s2 false // String s3 = "ab" + "c"; s3 == s2 true System.out.println(s3.getClass()); System.out.println(s3 == s2); } }
当字符串与变量进行拼接时,实际上是创建了一个StringBuilder对象,并使用了append(String)方法进行了拼接,最终调用StringBuilder的toString()返回一个字符串对象,所以变量和字符串拼接的结果不会被存储进常量池中(可以通过反编译进行校验),而是在堆内存中
而当字符串和字符串进行拼接时,实际上他们在编译器就已经合并成了一个字符串(可以通过反编译进行校验)
-
String是被final修饰的类
-
String的equals()是先判断是否为同一个对象,如果是直接返回true,再判断是不是String类型的对象,如果不是直接返回false,如果是则判断他们的值是否相同。
public boolean equals(Object anObject) { if (this == anObject) { //判断是不是同一个对象 return true; } if (anObject instanceof String) { //判断是不是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; //只要有一个字符不相等,直接返回false i++; } return true; } } //anObject对象不是String类型或者两个对象的数组长度不同 return false; }
StringBuffer
- StringBuffer的底层是一个字符数组(jdk9之前,9包括9之后是一个byte数组),StringBuffer是一个线程安全的可变字符序列,但是因为线程安全所以效率比线程不安全的StringBuilder要低
- 被final修饰,是一个最终类,不可以被继承,也就是没有子类
- StringBuffer初始容量为16(也就是字符数组的长度),还有一个属性count用于记录字符数组中存放了几个字符,不等同于容量
- 每次扩容为原来容量的的两倍+2,最大容量为Integer.MAX_VALUE
- StringBuffer的toString()实际上是创建一个String对象
- 多线程下,如果字符串频繁拼接时可以用StringBuffer,这样安全
StringBuilder
-
StringBuilder是一个字符数组(jdk9之前,9包括9之后是一个byte数组)
-
被final修饰,是一个最终类,不可以被继承,也就是没有子类
-
StringBuilder是一个线程不安全的可变字符序列
-
StringBuiler初始容量是16,还有一个属性count用于记录字符数组中存放了几个字符,不等同于容量
-
每次扩容为原来容量的的两倍+2,最大容量为Integer.MAX_VALUE
-
StringBuffer的toString()实际上是创建一个String对象
-
单线程下,如果字符串频繁拼接时可以用StringBuilder,这样效率更高
反射