java基础
一、什么是类、对象
类是具有共同属性和行为的对象集合,类定义了对象的属性和方法。可以说,类就是一个模板,定义多个对象共同的属性和方法,是抽象概念。
对象是类的具体实现;对象是通过引用来操作的
二、java的数据类型
1.基本数据类型与引用数据类型
基本数据类型:boolean、float(单精度浮点型)、char、byte(字节型)、short(短整型)、int(整型)、long(长整型)、double(双精度浮点型)
类型名称 | 关键字 | 占用内存 | 取值范围 |
---|---|---|---|
字节型 | byte | 1 字节 | -128~127 |
短整型 | short | 2 字节 | -32768~32767 |
整型 | int | 4 字节 | -2147483648~2147483647 |
长整型 | long | 8 字节 | -9223372036854775808L~9223372036854775807L |
单精度浮点型 | float | 4 字节 | +/-3.4E+38F(6~7 个有效位) |
双精度浮点型 | double | 8 字节 | +/-1.8E+308 (15 个有效位) |
字符型 | char | 2 字节 | ISO 单一字符集 |
布尔型 | boolean | 1 字节 | true 或 false |
引用数据类型:类、接口、数组。
也有人说除了基本类型外就是引用类型
为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java为每 一个基本数据类型都引入了对应的包装类型(也是引用类型),int的包装类就是Integer,从Java 5开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
基本数据类型: boolean,char,byte,short,int,long,float,double
封装类类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
缓冲类类型:IntBuffer,CharBuffer,ByteBuffer,ShortBuffer,DoubleBuffer
所有的类型在内存中都会分配一定的存储空间(形参在使用的时候也会分配存储空间,方法调用完成之后,这块存储空间自动消失), 基本的变量类型只有一块存储空间(分配在stack(栈)中), 而引用类型有两块存储空间(一块在stack中,一块在heap(堆)中)
public class Test {
//交换两个变量的值
public static void Swap(int a,int b){
int c=a;
a=b;
b=c;
System.out.println("a: "+a);
System.out.println("b: "+b);
}
public static void main(String[] args){
int c=10;
int d=20;
Swap(c,d);
System.out.println("After Swap:");
System.out.println("c: "+d);
System.out.println("d: "+c);
}
}
不难看出,虽然在 Swap (a,b) 方法中改变了传进来的参数的值,但对这个参数源变量本身并没有影响,即对 main(String[]) 方法里的 a,b 变量没有影响。那说明,参数类型是简单类型的时候,是按值传递的。以参数形式传递简单类型的变量时,实际上是将参数的值作了一个拷贝传进方法函数的,那么在方法函数里再怎么改变其值,其结果都是只改变了拷贝的值,而不是源值。想要改变就得使用引用数据类型;类似于c中的指针;
引用类型
和c中的指针一样; 引用是一种数据类型(保存在stack中),保存了对象在内存(heap,堆空间)中的地址,这种类型即不是我们平时所说的简单数据类型也不是类实例(对象);
package test;
public class Test {
public static void Sample(StringBuffer a){
a.append(" Changed ");
System.out.println("a: "+a);
}
public static void main(String[] args){
StringBuffer b=new StringBuffer("This is a test!");
Sample(b);
System.out.println("b: "+b);
}
}
可以看出值改变了
一个例子:String a=“hahaha”
**引用:**a
引用类型:String
通过引用a来操作存放在堆中的对象
2.装箱和拆箱
装箱是将基本类型装换成包装类型的过程;拆箱就是将包装类型转换成基本类型的过程;
在JDK1.5之前,要生成一个数值为1的Integer对象,需这样写:
Integer i = new Integer(1);
从JDK1.5开始就提供了自动装箱的功能,要生成一个数值为1的Integer对象,只需:
Integer i = 1;(这个过程中会自动根据数值创建对应的 Integer对象,这就是装箱。)
举例:
Integer i = 1; //装箱(定义的时候自动进行装箱)
int j = i; //拆箱(进行赋值或者逻辑运算时会自动进行拆箱)(将引用类型Integer的i转换成值类型int的i,然后进行赋值或者四则运算)Java是一种完全面向对象的语言。因此,包括数字、字符、日期、布尔值等等在内的一切,都是对象。似乎只需要一种方式来对待这些对象就可以了。
对于CPU来说,处理一个完整的对象,需要很多的指令,对于内存来说,又需要很多的内存。如果连整数都是对象,那么性能自然很低。
于是创造了这样一种机制,使得这些基本类型在一般的编程中被当作非对象的简单类型处理,在另一些场合,又允许它们被视作是一个对象。
这就是装箱和拆箱。
作用:为了保证通用性和提高系统性能
int装箱的时候自动调用Integer的valueOf(int)方法;Integer拆箱的时候自动调用Integer的intValue方法。
拆箱是通过调用包装器类的 xxxValue 方法实现的,xxx代表对应的基本数据类型(int,short,long,double,byte,char,float,boolean)。
每个包装器类都具有这两个方法
Sting s=“zzl”;这也是用的装箱实现的;
否则就要这样写:String s=new String(“zzl”);
String的值都在方法区的常量池中,前者要在对中创建s对象和常量池中创建hello对象。后者只需要在常量池中创建hello对象; 前者会创建2个对象,后者创建1个对象。
其实String是一个复杂的类,里面编写了很多方法;这里的方法区涉及到了JVM后面会细说
三、java 集合
1.什么是集合,为什么要使用集合,集合于数组的区别
-
集合是一个用来存放对象的容器
注意:①、集合只能存放对象(引用)。比如你存一个 int 型数据 1放入集合中,其实它是自动转换成 Integer 类后存入的,Java中每一种基本类型都有对应的引用类型。
②、集合存放的是多个对象的引用,集合的申明(引用)放在栈中,对象本身还是放在堆内存中。
③、集合可以存放不同类型,不限数量的数据类型。
-
我们最初学习的时候数组是用的很多的,都明白数组的缺点,需要预先设置好数组的大小和类型,我们往往不能提前知道需要多少的空间,所以只能设置一个用不完的大小;这时候我们会想要是有个能存放随意数量、任意类型的数组就好了;集合就应运而生了
2.Conllection
ArrayList:底层还是使用的数组;
*数组补充(*后面分析ArrayList源码时会用到):java中数组的本质也是对象,所有我们才可用用a.length方法;不过这个对象不是由某个类实例化产生的,而是由JVM创建的
public class Test02 {
public static void main(String[] args) {
int [] a={0,1,2};
int [] b={0};
b=a;
for(int a2:b){
System.out.println(a2);
}
}
}
控制台输出结果为0、1、2;所以我们好像感觉数组a扩容了,其实并没有;这里其实是将a指向的对象地址赋值给了b;b现在指向的的就是a指向的那个地址的数组;原来b指向的地址相当于没用了;
通过hashcode()查看的地址也跟我们所想的一样(当然不同类型的对象是不能使用=号的啊)
ArrayList简介:
ArrayList是基于数组实现的,有序的,查询快On(1)、增删慢On(n);其实ArrayList底层如果我们不去手动设置(trimTosize),还是会浪费空间的
也相当于设置了一个用不完的数组
ArrayList源码分析:
private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
private int size;
DEFAULTCAPACITY_EMPTY_ELEMENTDATA 在无参构造中调用赋值为空数组
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
EMPTY_ELEMENTDATA当有参构造函数的参数为0时,将其赋值给elementData
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);
}
}
transient Object[] elementData;创建了一个对象类型的数组用来存储 ArrayList 内的元素
注:这里的transient关键字。就是去掉序列化(因为ArrayList本身就是实现了序列化的)
private int size;size 表示它包含的元素的数量
还有一点这里的elementData没有设置为private是为了简化内部类的访问
由于ArrayList中的方法太多。这里只研究最总要的
这里是ArrayList的三个构造函数
这里的功能很简单,就是进行初始化操作
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 ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
**trimTosize**是将ArrayList底层的数组elementData[]的长度更改为实际的大小(size)可以节省空间
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
ensureCapacity是直接为ArrayList底层的数组elementData[]设置一个初始的大小。不用慢慢的进行扩容操作,可以提升效率
public void ensureCapacity(int minCapacity) {
if (minCapacity > elementData.length
&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
&& minCapacity <= DEFAULT_CAPACITY)) {
modCount++;
grow(minCapacity);
}
}
(重点) grow是ArrayList能够不用设置大小就可以进行存储的关键
这里要联合起下面的add()一起看
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
private Object[] grow() {
return grow(size + 1);
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
/*
在index出插入 element
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element;
size = s + 1;
}
假设初始化elementData[]为空,length为0,调用add(1),此处会调用add(1.elementData,size),现在size还是0;进入
add(1.elementData,size)后调用grow(),grow会调用 grow(size + 1);此时minCapacity为1, oldCapacity为-=0;
调用return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
该类认为你是第一次进行操作,直接为elementData赋值为默认的大小10;
假如你已经添加完了10个值,现在调用add(11),前面的操作都是一样,此时会调用
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
newLength(oldCapacity,minCapacity - oldCapacity, oldCapacity >> 1) 是在oldCapacity和minCapacity - oldCapacity中选出最大值然后加上oldCapacity >> 1(这个符号相当于%2),所有新生成的大小为原来大小的1.5倍。这就是ArrayList的扩容机制
LinkedList简介:
LinkedList底层是基于链表实现的;LinkedList是双向链表
LinkedList源码分析:
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
size记录链表中实际存储的数量;first始终指向头节点;last始终指向尾节点;
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
这里定义了节点的格式 data就是这里的item和element
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8rlXMA3I-1649834041557)(D:\笔记.md\图片\1.png)]
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
因为链表不需要设置空间大小,所以使用构造函数创造一个空链表;this()的作用是调用本类中的其他构造函数;
public boolean add(E e) {
linkLast(e);
return true;
}
add默认添加在链表的尾部;
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
首先将last指向l;新建一个newNode对象;此时这个对象作为链表中的最后一个节点,所以将last指向newNode;如果l为空,即前一个节点为空,这种情况就是第一次添加元素的时候,所以将first也指向newNode;如果不是就前一个节点的next指向newNode;最后将size++
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
首先由在**remove**中通过data(item,element)中的数据获取出这个节点(对象)然后调用 unlink删除节点;unlink就是将被删除节点的前后节点连接在一起。然后再将被删除节点的prev和next赋值为空,最后由GC来处理这些没有引用的对象;
HashSet简介:
HashSet是基于HashMaps实现的,实现了Set接口
是一个无序,不可重复的集合类,且线程不安全
HashSet源码分析:
HashSet底层的大多数代码都与HashMap有关
hashSet.add("zzl");
hashSet.add(1);
这里的zzl和1其实转换成了HashMap中的key。value是private static final Object PRESENT = new Object();
因为HashMap中的key是不允许重复的,所以HashSet存储的值也是不允许重复的
其实就是相同的key会覆盖;
HashMap简介:
HashMap是java里以Key-value存储的一种集合;在jdk1,7中使用的是数组加链表;1.8中使用的数组加链表加红黑树
HashMap允许k-v为null,是一种无序且线程不安全的集合对象
HashMap源码分析:
这里主要以jdk1.8以上的版本进行分析
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
- 定义了最大容量为2的30次方
- 默认的负载为0.75(问题:为什么要设置为0.75)
- TREEIFY_THRESHOLD 当数组某下标上的链表长度大于8时,将其转换为红黑树(问题:为什么选择红黑树而不是其他)
- UNTREEIFY_THRESHOLD 将红黑树转为链表的阈值,当在扩容(resize())时(此时HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 < 6时,则将 红黑树转换成链表
- MIN_TREEIFY_CAPACITY:当数组中某一个下标中的链表的元素大于了8,但此时数组的容量小于64,此时不会将链表转换成红黑树,而是将数组扩容,当数组的容量>64,此时下标上的元素大于8时才会将起其转换成红黑树
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;
}
-
这里定义了节点的形式
-
这个hash值的作用:提升散列性后用来寻找
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pm7o6thT-1649834041557)(D:\笔记.md\图片\fsfaf.png)]
transient Node<K,V>[] table;
HashMap底部存储使用的数组
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
-
这里是将由key得出的hashCode赋值给h,再将h的值右移16位与h进行异或运算;
这里为什么要进行右移处理?举个例子
假如k=181(Integer类型的hashCode值为他自己)如果不进行右移处理,假如选一个15进行异或
h: 1011 0101、 15:0000 1111 ^: 1011 1010
一个int类型就占用了4个字节32为,integer占用的字节会大于int类型。可以看出前面很多高位都没被使用;这样就会导致计算出来的hash值很容易重复,使用右移的操作可以使它的高位也参与进来,提高散列性
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
/**
* 该方法的作用是返回无符号整型i的最高非零位前面的0的个数,包括符号位在内;
*/
public static int numberOfLeadingZeros(int i) {
// HD, Count leading 0's
if (i <= 0)
return i == 0 ? 32 : 0;
int n = 31;
if (i >= 1 << 16) { n -= 16; i >>>= 16; }
if (i >= 1 << 8) { n -= 8; i >>>= 8; }
if (i >= 1 << 4) { n -= 4; i >>>= 4; }
if (i >= 1 << 2) { n -= 2; i >>>= 2; }
return n - (i >>> 1);
}
-
这里的tableSizeFor容量必须是2的幂,通过此方法计算得到大于给定容量的最小的2的幂
-
这里与jdk1.8有所不同(本人使用的jdk15)
假设cap = 16, 转成二进制就是 0001 0000
cap - 1 = 15, 转成二进制就是 0000 1111
代入numberOfLeadingZeros()方法计算:1的二进制表示是 0000 0001
i = 15, 1 << 16 即是2的16次方 转成二进制是 1 00000000 0000000 ,i < (1 << 16)
i = 15, 1 << 8 即是2的8次方 转成二进制是 1 00000000, i < (1 << 8)
i = 15, 1 << 4 即是2的4次方 转成二进制是 0001 0000, i < (1 << 4)
i = 15, 1 << 2 即是2的2次方 转成二进制是 0000 0100, i > (1 << 2), n = 31 - 2 = 29 , i >>> 2 转成二进制 0000 0011 也就是 3
return n - ( i >>> 1 ) = 29 - (i >>> 1 二进制是 0000 0001 , 十进制是1) = 28注意:这里为什么是i >>>= 16; i >>>= 8; i >>>= 4; i >>>= 2; i >>> 1 ?
16 + 8 + 4 + 2 + 1 = 31 int 是 4个字节 也就是32位。在计算机中,负数以其正值的补码形式表达。
原码:一个整数,按照绝对值大小转换成的二进制数,称为原码。
反码:将二进制数按位取反,所得的新二进制数称为原二进制数的反码。取反操作指:原为1,得0;原为0,得1。(1变0; 0变1)
补码:反码加1称为补码。-1 的二进制表示 是 11111111 11111111 11111111 11111111 右移28位就是 0000 1111 转成十进制是 15
tableSizeFor(15)拿到的n = 15 0 < 15 < MAXIMUM_CAPACITY,最后返回的结果是n + 1 = 16
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) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
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;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
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<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;
}
-
这里是HashMap的扩容操作;同时初始化也在这里面进行,也有对节点的一些操作
-
1.HashMap hashmap=new HashMap();
此时会走最后一个分支
else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); }
数组容量为16;阈值为12;
2.HashMap hashMap=new HashMap(22);
此时会调用有参构造
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }
这里通过上面分析最后的这个threshold阈值为输入值的最小的2的幂次;所以此时threshold=32;
会走这个分支:
else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr;
此时newCap=32;newThr=32*0.75=24;(不要去想前面的那个例子,这里相当于是个新的HashMap,里面的东西都还是没有赋值过的)
3.已经插满24个元素后此时需要扩容
走第一个分支,oldCap=32,oldThr=24扩容:newCap = oldCap << 1 为64 阈值容量newThr = oldThr << 1 为48
以上就是HashMap的扩容机制;
-
接下来看HashMap的put过程
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * Implements Map.put and related methods. * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ 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; }
-
调用put的时候会进入putVal根据传来的key计算出它的hash值(为了后面计算数组下标的时候使用);
-
1.首先判断数组是否为空。如果为空就去resize中进行初始化操作(1.7中是先进行初始化后再进行的put操作)
-
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
这里的是通过hash与数组的长度计算出数组的下标,判断这个位置是否为空,为空就将这个元素put再这个位置
从这里可以看出HashMap是通过数组长度和key产生的hash值来存放到数组中的位置上
(n-1)&hash其实就是取模(限制数字的范围,不会造成数组越界的情况),不过采用位运算的效率更高;
-
2.如果当前位置的值不为空,判断数组当前位置的存储的key,相同则替换,不同则判断该位置上连接的是红黑树还是链表如果是红黑树则去调用putTreeVal添加到红黑树中;如果是链表则使用尾插法插入到链表中
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; } }
四、抽象类与接口
1.什么是抽象类
- 拥有抽象方法的类必为抽象类;
- 抽象类必须要用abstract关键字修饰;
抽象类的总结:
-
抽象类是一种模板思想;
-
抽象类不能被实例化(初学者很容易犯的错),如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。
-
抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
-
抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。
-
构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。
-
抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。
2.什么是接口
- 接口是一个抽象方法的集合。
- 接口通过implements实现
接口的总结:
- 接口不能用于实例化对象。
- 接口没有构造方法。
- 接口中所有的方法必须是抽象方法,Java 8 之后 接口中可以使用 default 关键字修饰的非抽象方法。
- 接口不能包含成员变量,除了 static 和 final 变量。
- 接口不是被类继承了,而是要被类实现。
- 接口支持多继承。
3.接口和抽象类的区别
-
抽象类可以给出一些成员的实现,接口却不包含成员的实现,抽象类的抽象成员可被子类部分实现,接口的成员需要实现类完全实现,一个类只能继承一个抽象类,但是可以实现多个接口等。
-
**类是对对象的抽象,抽象类是对类的抽象,接口是对行为的抽象。**接口是对类的局部(行为)进行抽象,而抽象类是对类整体(字段、属性、方法)的抽象。不论接口、抽象类、类、甚至对象,都是在不同层次、不同角度进行抽象的结果。他们的共性就是抽象。
如果行为跨域不同类的对象(飞机、麻雀、超人),可使用接口;对于一些相似的类的对象(猫狗),可继承抽象类。
-
抽象类是从子类中发现公共的东西,泛化出父类,然后子类继承父类,而接口是根本不知道子类的存在,方法如何实现还不确定,预先定义。
-
抽象类是自底向上抽象出来的,而接口则是自顶向下设计出来的。
1.为什么有了接口还需要抽象类?
让抽象类去实现接口,而不是每个实现类去实现接口,实现类一致的行为(或者说大部分一致的行为)在抽象类中进行实现,而不一致的行为(或者一小部分不一致的行为),在抽象类中写成抽象方法,让子类去重写,减少子类重写接口所有方法的负担。
2.什么时候使用接口、什么时候使用抽象类?
五、枚举
什么是枚举?
- 枚举是一个特殊的类
- 枚举中的元素其实就是它的对象
- values() 返回枚举类中所有的值。
- ordinal()方法可以找到每个枚举常量的索引,就像数组索引一样。
- valueOf()方法返回指定字符串值的枚举常量。
六、泛型
什么是泛型?
泛型就是参数化类型;将原来具体的类型参数化,类似于方法中的变量参数。可以提高代码的复用性
同时泛型也是一种安全机制,可以将运行时的异常提前到编译期
泛型的三种使用方式
- 1.泛型类
泛型类就是把泛型定义在类上,用户使用该类的时候,才把类型明确下来
这样的话,用户明确了什么类型,该类就代表着什么类型…用户在使用的时候就不用担心强转的问题,运行时转换异常的问题了。
- 2.泛型接口
public class Test5<String> implements fanxing<String>{
@Override
public String fun() {
return null;
}
}
泛型接口就是在接口中定义泛型;如果接口中定义泛型方法;在实现类中的泛型方法的类型为实现类中指定的类型
如果实现类中不指定泛型接口的类型,就会默认为Object;
- 泛型方法
在方法前设置泛型方法会自动根据传递进来的参数自动的判断引用类型;
public class Test6 {
public <T> T hun(T t){
System.out.println(t);
return t;
}
public static void main(String[] args) {
Test6 test6=new Test6();
test6.hun("zzl");
test6.hun(123);
}
}
七、反射
Java反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。而这也是Java被视为动态(或准动态,为啥要说是准动态,因为一般而言的动态语言定义是程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言。从这个观点看,Perl,Python,Ruby是动态语言,C++,Java,C#不是动态语言。)语言的一个关键性质。
我们知道反射机制允许程序在运行时取得任何一个已知名称的class的内部信息,包括包括其modifiers(修饰符),fields(属性),methods(方法)等,并可于运行时改变fields内容或调用methods。那么我们便可以更灵活的编写代码,代码可以在运行时装配,无需在组件之间进行源代码链接,降低代码的耦合度;还有动态代理的实现等等;但是需要注意的是反射使用不当会造成很高的资源消耗!
-
Class
类的实例表示中运行的 Java 应用程序类和接口。枚举是一种类和注释是一种接口。每个数组也属于一个类,则体现为一个Class
对象,由相同的元素类型和维数的所有阵列共享。原始的 Java 类型 (boolean
、byte
、char
、short
、int
、long
、float
和double
) 和关键字void
也表示为Class
对象。Class
有没有公共构造函数。相反Class
构建这些对象会自动由 Java 虚拟机装载类,以及通过调用defineClass
方法在类加载器。注:这里的注释不是/ // /**/,而是@Test、 @SuppressWarnings(“uncheck”)
1了解Class类
java中一切皆对象。某种意义上来说java中只存在两种对象,一种是实例对象,一种是Class对象。每个类的运行时的类型信息就是用class对象表示的。它包含了与类有关的信息.Java使用Class对象执行其RTTI(运行时类型识别,Run-Time Type Identification),多态是基于RTTI实现的。
每一个类都有一个Class对象,每当编译一个新类就产生一个Class对象,基本类型 (boolean, byte, char, short, int, long, float, and double)有Class对象,数组有Class对象,就连关键字void也有Class对象(void.class)。Class对象对应着java.lang.Class类,如果说类是对象抽象和集合的话,那么Class类就是对类的抽象和集合。
Class类没有公共的构造方法,Class对象是在类加载的时候由Java虚拟机以及通过调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个Class对象。一个类被加载到内存并供我们使用需要经历如下三个阶段:
加载,这是由类加载器(ClassLoader)执行的。通过一个类的全限定名来获取其定义的二进制字节流(Class字节码),将这个字节流所代表的静态存储结构转化为方法去的运行时数据接口,根据字节码在java堆中生成一个代表这个类的java.lang.Class对象。 链接。在链接阶段将验证Class文件中的字节流包含的信息是否符合当前虚拟机的要求,为静态域分配存储空间并设置类变量的初始值(默认的零值),并且如果必需的话,将常量池中的符号引用转化为直接引用。 初始化。到了此阶段,才真正开始执行类中定义的java程序代码。用于执行该类的静态初始器和静态初始块,如果该类有父类的话,则优先对其父类进行初始化。
**所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。**当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new创建类对象的时候也会被当作对类的静态成员的引用。因此java程序程序在它开始运行之前并非被完全加载,其各个类都是在必需时才加载的。这一点与许多传统语言都不同。动态加载使能的行为,在诸如C++这样的静态加载语言中是很难或者根本不可能复制的。
在类加载阶段,类加载器首先检查这个类的Class对象是否已经被加载。如果尚未加载,默认的类加载器就会根据类的全限定名查找.class文件。在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良java代码。一旦某个类的Class对象被载入内存,我们就可以它来创建这个类的所有对象。
2.获取Class对象的三种方法
- Class.forName(“类的全限定名”),可能会出现异常,需要对异常进行处理
- 实例对象.getClass()
- 类名.class (类字面常量)
3.与反射机制相关的类和方法
类名 | 用途 |
---|---|
Class类 | 代表类的实体,在运行的Java应用程序中表示类和接口 |
Field类 | 代表类的成员变量(成员变量也称为类的属性) |
Method类 | 代表类的方法 |
Constructor类 | 代表类的构造方法 |
方法 | 用途 |
---|---|
asSubclass(Class clazz) | 把传递的类的对象转换成代表其子类的对象 |
Cast() | 把对象转换成代表类或是接口的对象 |
getClassLoader() | 获得类的加载器 |
getClasses() | 返回一个数组,数组中包含该类中所有公共类和接口的对象 |
getDeclaredClasses() | 返回一个数组,数组中包含改类的所有类和接口的对象 |
forName() | 根据类的全限定名返回类的对象 |
getName() | 获得类的完整路径名字 |
newInstance() | 创建类的实例 |
getPackage() | 获得类的包 |
方法 | 用途 |
---|---|
getField(String name) | 获得某个公有的属性对象 |
getFields() | 获得所有公有的属性对象 |
getDeclaredField(String name) | 获得某个属性对象 |
getDeclaredFields() | 获得所有属性对象 |
4.反射机制的具体体现
1.Jdbc数据库的连接
-
通过Class.forName()加载数据库的驱动程序 (通过反射加载,前提是引入相关了Jar包)
-
通过 DriverManager 类进行数据库的连接,连接的时候要输入数据库的连接地址、用户名、密码
-
通过Connection 接口接收连接
public class ConnectionJDBC { /** * @param args */ //驱动程序就是之前在classpath中配置的JDBC的驱动程序的JAR 包中 public static final String DBDRIVER = "com.mysql.jdbc.Driver"; //连接地址是由各个数据库生产商单独提供的,所以需要单独记住 public static final String DBURL = "jdbc:mysql://localhost:3306/test"; //连接数据库的用户名 public static final String DBUSER = "root"; //连接数据库的密码 public static final String DBPASS = ""; public static void main(String[] args) throws Exception { Connection con = null; //表示数据库的连接对象 Class.forName(DBDRIVER); //1、使用CLASS 类加载驱动程序 ,反射机制的体现 con = DriverManager.getConnection(DBURL,DBUSER,DBPASS); //2、连接数据库 System.out.println(con); con.close(); // 3、关闭数据库 } }
2.spring框架
在 Java的反射机制在做基础框架的时候非常有用,行内有一句这样的老话:反射机制是Java框架的基石。一般应用层面很少用,不过这种东西,现在很多开源框架基本都已经封装好了,自己基本用不着写。 典型的除了hibernate之外,还有spring也用到很多反射机制。最经典的就是xml的配置模式。 Spring 通过 XML 配置模式装载 Bean 的过程:
- 将程序内所有 XML 或 Properties 配置文件加载入内存中
- Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息
- 使用反射机制,根据这个字符串获得某个类的Class实例
- 动态配置实例的属性
Spring这样做的好处是:
- 不用每一次都要在代码里面去new或者做其他的事情
- 以后要改的话直接改配置文件,代码维护起来就很方便了
- 有时为了适应某些需求,Java类里面不一定能直接调用另外的方法,可以通过反射机制来实现
public class BeanFactory {
private Map<String, Object> beanMap = new HashMap<String, Object>();
/**
* bean工厂的初始化.
* @param xml xml配置文件
*/
public void init(String xml) {
try {
//读取指定的配置文件
SAXReader reader = new SAXReader();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
//从class目录下获取指定的xml文件
InputStream ins = classLoader.getResourceAsStream(xml);
Document doc = reader.read(ins);
Element root = doc.getRootElement();
Element foo;
//遍历bean
for (Iterator i = root.elementIterator("bean"); i.hasNext();) {
foo = (Element) i.next();
//获取bean的属性id和class
Attribute id = foo.attribute("id");
Attribute cls = foo.attribute("class");
//利用Java反射机制,通过class的名称获取Class对象
Class bean = Class.forName(cls.getText());
//获取对应class的信息
java.beans.BeanInfo info = java.beans.Introspector.getBeanInfo(bean);
//获取其属性描述
java.beans.PropertyDescriptor pd[] = info.getPropertyDescriptors();
//设置值的方法
Method mSet = null;
//创建一个对象
Object obj = bean.newInstance();
//遍历该bean的property属性
for (Iterator ite = foo.elementIterator("property"); ite.hasNext();) {
Element foo2 = (Element) ite.next();
//获取该property的name属性
Attribute name = foo2.attribute("name");
String value = null;
//获取该property的子元素value的值
for(Iterator ite1 = foo2.elementIterator("value"); ite1.hasNext();) {
Element node = (Element) ite1.next();
value = node.getText();
break;
}
for (int k = 0; k < pd.length; k++) {
if (pd[k].getName().equalsIgnoreCase(name.getText())) {
mSet = pd[k].getWriteMethod();
//利用Java的反射极致调用对象的某个set方法,并将值设置进去
mSet.invoke(obj, value);
}
}
}
//将对象放入beanMap中,其中key为id值,value为对象
beanMap.put(id.getText(), obj);
}
} catch (Exception e) {
System.out.println(e.toString());
}
}
//other codes
}
3.动态代理
动态代理是基于反射实现的
package com.zzl.fs;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/*
* 动态代理
* 1.创建被代理的类与接口
* 2.创建一个实现接口InvocationHandler的类,必须实现接口的invoke方法
* 3.通过Proxy的静态方法newProxyInstance(),拥有三个参数:类加载器,一组接口,调用处理器生成动态代理类对象、创建一个代理对象(realSubjectProxy),
*
*
*
*
* */
public class test05 {
public static void main(String[] args) {
RealSubjectInterface realSubject=new RealSubject();
InvocationHandler handler =new InvocationHandlerImpl(realSubject); //动态代理类对象
ClassLoader loader=realSubject.getClass().getClassLoader();
Class []interfaces=realSubject.getClass().getInterfaces();
RealSubjectInterface realSubjectProxy= (RealSubjectInterface) Proxy.newProxyInstance(loader,interfaces,handler); //代理类对象
System.out.println(Proxy.newProxyInstance(loader,interfaces,handler).hashCode());
System.out.println("动态代理对象的类型:"+realSubjectProxy.getClass().getName());
//用动态代理类对象 调用 真实对象的方法
System.out.println(realSubjectProxy.sayhello("Jason zhang"));
}
}
interface RealSubjectInterface{
public String sayhello(String name);
public String sayGoodBye();
}
class RealSubject implements RealSubjectInterface{
@Override
public String sayhello(String name) {
// TODO 自动生成的方法存根
return "hello "+name;
}
@Override
public String sayGoodBye() {
// TODO 自动生成的方法存根
return "goodbye";
}
}
class InvocationHandlerImpl implements InvocationHandler{
private Object realSubject;
//private RealSubject realsubject;
public InvocationHandlerImpl(Object realSubject){
this.realSubject =realSubject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//在代理真实对象前可以添加额外操作
System.out.println("在用代理对象调用真实对象的方法之前,类似AOP的Before(),自己的操作:");
System.out.println("其调用的方法Method:"+method);
//当代理对象调用真实对象的方法时,其会自动跳转到代理对象关联的handler对象的invoke方法来进行调用
Object returnValue=method.invoke(realSubject,args);
//在代理真实对象之后可以添加额外操作
System.out.println("在用代理对象调用真实对象的方法之后,但是返回值returnValue之前,自己的操作?");
return returnValue;
}
}
结果
在用代理对象调用真实对象的方法之前,类似AOP的Before(),自己的操作:
其调用的方法Method:public native int java.lang.Object.hashCode()
在用代理对象调用真实对象的方法之后,但是返回值returnValue之前,自己的操作?
2046562095
动态代理对象的类型:com.zzl.fs.$Proxy0
在用代理对象调用真实对象的方法之前,类似AOP的Before(),自己的操作:
其调用的方法Method:public abstract java.lang.String com.zzl.fs.RealSubjectInterface.sayhello(java.lang.String)
在用代理对象调用真实对象的方法之后,但是返回值returnValue之前,自己的操作?
hello Jason zhang
为什么这里的强转要强转为接口类型,而不是接口实例类,在spring中也有相同的问题?
- Proxy.newProxyInstance(loader,interfaces,handler)生成了一个对象,这个对象来自于com.zzl.fs.$Proxy0类。同时这个类继承了
Proxy(注:这就是jdk的动态代理只能代理接口的原因),实现了我们自定义的接口;
-
这里为什么要强转为接口类型而不是接口的实现类:
1.这个方法返回的是一个Object对象如果不进行类型转化就无法使用;这个Object类型的对象强转为接口类型后就会生成接口实现类2.这个对象的类没有继承接口的实现类,如果转换为接口实现类,会产生类型产生错误的报错;
3.这个对象是实现了我们定义的接口的所以此时可以强转为接口类型;最后产生的是接口的实现类(实例化对象)
例:
Agg agg =new Test3();
System.out.println(agg.hashCode());
Test4 test4=new Test4();
//Agg sz=(Test3)(Object)test4;
Test3 test3=new Test3();
Agg sz=(Agg)(Object)test3;
Agg sss=(Test4)(Object)test3;
// Object t=(Object) test3;
// System.out.println(t.getClass().getInterfaces());
System.out.println(sz.hashCode());
这里虽然不会编译报错。但会运行报错
Exception in thread "main" java.lang.ClassCastException: class com.zzl.test.Test3 cannot be cast to class com.zzl.test.Test4 (com.zzl.test.Test3 and com.zzl.test.Test4 are in unnamed module of loader 'app')
at com.zzl.test.Test4.main(Test4.java:15)
可以看出这里的test3强转为(Object)对象后 又强转为Test4 失败。原因就是test3的类没有继承test4的是无法转换的(如果继承了就是可以使用接口的实现来进行强转的)
Agg sz=(Agg)(Object)test3;
修改为强转为(Agg)接口是可以的,因为test3本身的类实现Agg接口。(通过反射查看接口)转换为Object类型后也实现了这些接口
Object t=(Object) test3;
Class[] c=t.getClass().getInterfaces();
for(Class i:c){
System.out.println(i);
}
interface com.zzl.test.Add
interface com.zzl.test.Agg
八、IO、NIO、AIO
在我们学习Java的IO流之前,我们都要了解几个关键词
- 同步与异步(synchronous/asynchronous):同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系
- 阻塞与非阻塞:在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如ServerSocket新连接建立完毕,或者数据读取、写入操作完成;而非阻塞则是不管IO操作是否结束,直接返回,相应操作在后台继续处理
1.IO流(同步、阻塞)
什么是IO流
IO流简单来说就是input和output流,IO流主要是用来处理设备之间的数据传输,Java IO对于数据的操作都是通过流实现的,而java用于操作流的对象都在IO包中。是一种抽象概念。
按操作数据分为:字节流(InputStream、OutputStream)和字符流(Reader、Writer)
按流向分:输入流(Reader、InputStream)和输出流(Writer、OutputStream)
注:这里的输入和输出。输入是读的意思。输出是写的意思。
字符流
概述
只用来处理文本数据
数据最常见的表现形式是文件,字符流用来操作文件的子类一般是FileReader和FileWriter
字符流读写文件注意事项:
- 写入文件必须要用**flush()**刷新
- 用完流记得要关闭流
- 使用流对象要抛出IO异常
- 定义文件路径时,可以用"/“或者”"
- 在创建一个文件时,如果目录下有同名文件将被覆盖
- 在读取文件时,必须保证该文件已存在,否则抛出异常
字符流的缓冲区
- 缓冲区的出现是为了提高流的操作效率而出现的
- 需要被提高效率的流作为参数传递给缓冲区的构造函数
- 在缓冲区中封装了一个数组,存入数据后一次取出
public class IOTests2 {
public static void main(String[] args) throws IOException {
//创建源
File file=new File("D:/zzl.txt");
File file2=new File("D:/create.txt");
//选择流
Reader rd=new FileReader(file);
Writer wt=new FileWriter(file2);
char [] chars=new char[30];
int len=-1;
//操作流
while((len=rd.read(chars))>0){
wt.write(chars,0,len);
String str=new String(chars,0,len);
System.out.println(str);
}
wt.flush(); //没有装满缓冲区时需要强制刷新才会存入成功
rd.close(); //关闭源
}
}
字节流
概述
用来处理媒体数据
字节流读写文件注意事项:
- 字节流和字符流的基本操作是相同的,但是想要操作媒体流就需要用到字节流
- 字节流因为操作的是字节,所以可以用来操作媒体文件(媒体文件也是以字节存储的)
- 输入流(InputStream)、输出流(OutputStream)
- 字节流操作可以不用刷新流操作
- InputStream特有方法:int available()(返回文件中的字节个数)
字节流的缓冲区
字节流缓冲区跟字符流缓冲区一样,也是为了提高效率
//实现拷贝文件的效果
public class IOTest {
public static void main(String[] args) throws IOException {
//创建源
File file=new File("D:/create.txt");
File file2=new File("D:/zzl.txt");
//选择流
InputStream is=new FileInputStream(file);
OutputStream os=new FileOutputStream(file2);
//操作流
byte []bytes=new byte[30];
int len=-1;
while ((len=is.read(bytes))>0){ //通过read将文件中的数据以字节的形式存入bytes字节数组中。返回值为数据的长度,默认为1.
os.write(bytes,0,len);
}
is.close();
}
}
需要配合具体代码理解实际操作
java System类
System类代表系统,系统级的很多属性和控制方法都放置在该类的内部。该类位于java.lang包。
由于该类的构造方法是private的,所以无法创建该类的对象,也就是无法实例化该类。其内部的成员方法和成员变量都是static(静态)的,所以也可以很方便的调用他。
system中包含了in、out和err三个成员变量,分别代表标准输入流(键盘输入)、标准输出流(显示器)和标准错误输出流(显示器)
System.in可以从键盘获取输入的参数(与InputStream获取数据相同,通过字节数组操作);
byte [] bytes=new byte[30];
System.in.read(bytes);
String str=new String(bytes,0, bytes.length);
System.out.println(str);
也可以通过Sacnner来接受从键盘传来的数据
System.exit(0);退出虚拟机。退出后程序会彻底结束
gc也在这里面
System.getProperties()返回系统的一些属性
Java Scanner类
Java 5添加了java.util.Scanner类,这是一个用于扫描输入文本的新的实用程序
关于nextInt()、next()、nextLine()的理解
nextInt():只能读取数值,若是格式不对,会抛出java.util.InputMismatchException异常
next():遇见第一个有效字符(非空格,非换行符)时,开始扫描,当遇见第一个分隔符或结束符(空格或换行符)时,结束扫描,获取扫描到的内容
nextLine():可以扫描到一行内容并作为字符串而被捕获到
关于hasNext()、hasNextLine()、hasNextxxx()的理解
就是为了判断输入行中是否还存在xxx的意思
与delimiter()有关的方法
应该是输入内容的分隔符设置,
Scanner scan = new Scanner(System.in);
// 从键盘接收数据
// next方式接收字符串
System.out.println("next方式接收:");
// 判断是否还有输入
if (scan.hasNext()) {
String str1 = scan.next();
System.out.println("输入的数据为:" + str1);
}
scan.close();
在IO的接收中也可以使用Scanner 作为容器;
2.NIO(同步、非阻塞)
首先,我们要先了解一下NIO的三个主要组成部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器 在网络编程中使用)
IO操作往往在两个场景下会用到:
- 文件IO
- 网络IO
iO和NiO的区别
- 可简单认为:IO是面向流的处理,NIO是面向块(缓冲区)的处理
- 面向流的I/O 系统一次一个字节地处理数据。
- 一个面向块(缓冲区)的I/O系统以块的形式处理数据。
- 相对于传统IO而言,流是单向的。对于NIO而言,有了Channel管道这个概念,我们的读写都是双向的(铁路上的火车能从广州去北京、自然就能从北京返还到广州)!
- NIO被叫为
no-blocking io
,其实是在网络这个层次中理解的,对于FileChannel来说一样是阻塞。
NIO的使用方法
1.获取通道的方法
- 对象调用getChannel() 方法
FileInputStream fin=new FileInputStream("D:/zzl.txt");
FileChannel fc=fin.getChannel();
- 在jdk1.7以后可以通过FileChannel.open() 方式
FileChannel inChannel = FileChannel.open(Paths.get("D:/jpg"), StandardOpenOption.READ);
第二个参数是一个或多个打开选项,它告诉FileChannel
在底层文件上执行哪些操作。在本例中,我们使用了StandardOpenOption.READ
选项。阅读意味着该文件将被打开以供阅读。
2.测试例子
//客户端
public class NIO3 {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",6666));
FileChannel fileChannel=FileChannel.open(Paths.get("D:/zzl.jpg"), StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024*1024*3);
// 4.读取本地文件(图片),发送到服务器
while (fileChannel.read(buffer) != -1) {
// 在读之前都要切换成读模式
buffer.flip();
socketChannel.write(buffer);
// 读完切换成写模式,能让管道继续读取文件的数据
buffer.clear();
}
// 5. 关闭流
fileChannel.close();
socketChannel.close();
}
}
//服务器端
public class NIO4 {
public static void main(String[] args) throws IOException {
// 1.获取通道
ServerSocketChannel server = ServerSocketChannel.open();
// 2.得到文件通道,将客户端传递过来的图片写到本地项目下(写模式、没有则创建)
FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
// 3. 绑定链接
server.bind(new InetSocketAddress(6666));
// 4. 获取客户端的连接(阻塞的)
SocketChannel client = server.accept();
// 5. 要使用NIO,有了Channel,就必然要有Buffer,Buffer是与数据打交道的呢
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 6.将客户端传递过来的图片保存在本地中
while (client.read(buffer) != -1) {
// 在读之前都要切换成读模式
buffer.flip();
outChannel.write(buffer);
// 读完切换成写模式,能让管道继续读取文件的数据
buffer.clear();
}
// 7.关闭通道
outChannel.close();
client.close();
server.close();
}
}
九、多线程
1.什么是线程
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
2.什么时候使用多线程
当多个任务可以并行执行时,可以为每个任务启动一个线程。
3.多线程的基本使用
实现多线程的三种方式
- 继承Thread类 重写run方法
- 实现Runnable接口 重写run方法
- 通过 Callable 和 Future 创建线程 重写call方法
基本使用比较简单看看实例
public class DXCTest extends Thread{
public void run() {
for(int i=0;i<20;i++){
System.out.println("一边听歌");
}
}
public static void main(String[] args) throws InterruptedException {
DXCTest dxcTest=new DXCTest();
dxcTest.start();
for(int i=0;i<20;i++){
System.out.println("一边看电影");
}
}
}
Thread类的对象才能调用start()。如果是单单实现了Runnable、Callable接口,需要通过将其转换成线程类才能调用start();
public class DXCTest1 implements Runnable{
@Override
public void run() {
for(int i=0;i<20;i++){
System.out.println("一边听歌");
}
}
public static void main(String[] args) {
DXCTest1 dxcTest1=new DXCTest1();
// new Thread(dxcTest1).start();
for(int i=0;i<20;i++){
System.out.println("一边看电影");
}
new Thread(dxcTest1).start();
}
}
Call方法具有返回值,且能够抛出异常。
public class DXCTest2 implements Callable<Integer> {
public static void main(String[] args)
{
DXCTest2 dxcTest2= new DXCTest2();
FutureTask<Integer> ft = new FutureTask<>(dxcTest2);
for(int i = 0;i < 100;i++)
{
System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
if(i==20)
{
new Thread(ft,"有返回值的线程").start();
}
}
try
{
System.out.println("子线程的返回值:"+ft.get());
} catch (InterruptedException e)
{
e.printStackTrace();
} catch (ExecutionException e)
{
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception
{
int i = 0;
for(;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
}
常用方法
- Sleep()
(Thread类的静态方法)让线程休息指定的秒数,不会释放锁,不会释放资源。转换成
- yield()
(Thread类的静态方法)暂停当前正在执行的线程对象,放弃cpu。从运行态转换为就绪态
- join()
(Thread的成员方法)让主线程等待实现了join的方法的线程,知道该线程结束,主线程才能运行
- wait()
(Object的成员方法) 在同步块中使用,将实习了wait()的线程阻塞。
- notify()
(Object的成员方法)唤醒被wait阻塞的线程
- interrupt()
(Thread类的成员方法) 将被阻塞的线程中断。可以用来提前结束sleep()
4.锁(synchronized和lock)
1.为什么要使用锁?
为了避免资源竞争产生的错误。在Java中,是没有类似于PV操作、进程互斥等相关的方法的。JAVA的进程同步是通过synchronized()来实现的,需要说明的是,Java的synchronized()方法类似于操作系统概念中的互斥内存块,在Java中的Object类对象中,都是带有一个内存锁的,在有线程获取该内存锁后,其它线程无法访问该内存,从而实现Java中简单的同步、互斥操作。
2.synchronized
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
3.lock
lock是一个接口,两个直接实现类:ReentrantLock(重入锁), ReentrantReadWriteLock(读写锁)。
public class MyLockStudy implements Runnable {
private int count;
Lock l = new ReentrantLock();
@Override
public void run() {
l.lock();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": ");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
l.unlock();
}
public static void main(String args[]) {
MyLockStudy runn = new MyLockStudy();
Thread thread1 = new Thread(runn, "thread1");
Thread thread2 = new Thread(runn, "thread2");
Thread thread3 = new Thread(runn, "thread3");
thread1.start();
thread2.start();
thread3.start();
}
tryLock(),锁在空闲的才能获取锁(未获得锁不会等待,lock()未获得锁会进入阻塞状态 ),返回值为boolean类型,获取锁则为 true ,反之为 false
public class MyLockStudy implements Runnable {
private int count;
Lock l = new ReentrantLock();
@Override
public void run() {
if (l.tryLock()) {
System.out.println(Thread.currentThread().getName() + "获取锁");
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": ");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
l.unlock();
} else {
System.out.println(Thread.currentThread().getName() + "未获取锁");
}
}
public static void main(String args[]) {
MyLockStudy runn = new MyLockStudy();
Thread thread1 = new Thread(runn, "thread1");
Thread thread2 = new Thread(runn, "thread2");
Thread thread3 = new Thread(runn, "thread3");
thread1.start();
thread2.start();
thread3.start();
}
}
结果
thread2未获取锁
thread3未获取锁
thread1获取锁
thread1:
thread1:
thread1:
thread1:
thread1:
tryLock(long time, TimeUnit unit) 方法
如果锁定可用,则此方法立即返回值true
。如果锁不可用,则当前线程将被禁用以进行线程调度,并且在发生以下三种情况之一之前处于休眠状态:
- 当前线程获取锁。
- 其他一些线程中断当前线程。
- 等待时间过去了,返回false
public class MyLockStudy implements Runnable {
private int count;
Lock l = new ReentrantLock();
@Override
public void run() {
try {
if (l.tryLock(1000, TimeUnit.MILLISECONDS)) {
System.out.println(Thread.currentThread().getName() + "获取锁");
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": ");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
l.unlock();
} else {
System.out.println(Thread.currentThread().getName() + "未获取锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String args[]) {
MyLockStudy runn = new MyLockStudy();
Thread thread1 = new Thread(runn, "thread1");
Thread thread2 = new Thread(runn, "thread2");
Thread thread3 = new Thread(runn, "thread3");
thread1.start();
thread2.start();
thread3.start();
}
}
4.lock与synchronized的区别
存在层次上
synchronized: Java的关键字,在jvm层面上
Lock: 是一个接口
锁的释放
synchronized: 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁
Lock: 在finally中必须释放锁,不然容易造成线程死锁
锁的获取
synchronized: 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待
Lock: 分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待(可以通过tryLock判断有没有锁)
锁的释放(死锁产生)
synchronized: 在发生异常时候会自动释放占有的锁,因此不会出现死锁
Lock: 发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生
锁的状态
synchronized: 无法判断
Lock: 可以判断
锁的类型
synchronized: 可重入 不可中断 非公平
Lock: 可重入 可判断 可公平(两者皆可)
性能
synchronized: 少量同步
Lock: 大量同步
- Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
- 在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
- ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。
调度
synchronized: 使用Object对象本身的wait 、notify、notifyAll调度机制
Lock: 可以使用Condition进行线程之间的调度
用法
synchronized: 在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
Lock: 一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
底层实现
synchronized: 底层使用指令码方式来控制锁的,映射成字节码指令就是增加来两个指令:monitorenter和monitorexit。当线程执行遇到monitorenter指令时会尝试获取内置锁,如果获取锁则锁计数器+1,如果没有获取锁则阻塞;当遇到monitorexit指令时锁计数器-1,如果计数器为0则释放锁。
Lock: 底层是CAS乐观锁,依赖AbstractQueuedSynchronizer类,把所有的请求线程构成一个CLH队列。而对该队列的操作均通过Lock-Free(CAS)操作。
5.线程池
1**.为什么要使用线程池**
我们知道不用线程池的话,每个线程都要通过 new Thread(xxRunnable).start()的方式来创建并运行一个线程,线程少的话这不会是问题,而真实环境可能会开启多个线程让系统和程序达到最佳效率,当线程数达到一定数量就会耗尽系统的 CPU 和内存资源,也会造成 GC频繁收集和停顿,因为每次创建和销毁一个线程都是要消耗系统资源的,如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。所以, 线程池中的线程复用极大节省了系统资源,当线程一段时间不再有任务处理时它也会自动销毁,而不会长驻内存。
2.线程池的创建方式
线程池的顶级接口是Executor;在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;
然后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;
抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;
然后ThreadPoolExecutor继承了类AbstractExecutorService。
因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。(灵活度太低,不建议使用)
ExecutorService executorService= Executors.newFixedThreadPool(2);
通常使用手动创建线程池的方式
ThreadPoolExecutor executor=new ThreadPoolExecutor(2, 4, 10, TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t=new Thread();
t.setName("zzl");
return t;
}
}
,new ThreadPoolExecutor.AbortPolicy()
);
线程池的参数如下:
一、corePoolSize 线程池核心线程大小
二、maximumPoolSize 线程池最大线程数量
三、keepAliveTime 空闲线程存活时间
四、unit 空闲线程存活时间单位
五、workQueue 工作队列
六、threadFactory 线程工厂
七、handler 拒绝策略
3.线程池的具体案例
1:编写任务类(MyTask),实现Runnable接口;
2:编写线程类(MyWorker),用于执行任务,需要持有所有任务;
3:编写线程池类(MyThreadPool),包含提交任务,执行任务的能力;
4:编写测试类(MyTest),创建线程池对象,提交多个任务测试;
public class MyTask implements Runnable{
private int id;
@Override
public String toString() {
return "MyTask{" +
"id=" + id +
'}';
}
@Override
public void run() {
String name =Thread.currentThread().getName();
System.out.println("线程:"+name+"即将执行任务:"+id);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:"+name+"完成了任务:"+id);
}
public MyTask(int id){
this.id=id;
}
}
public class MyWorker extends Thread{
private String name ;
private List<Runnable> tasks;
public MyWorker(String name, List<Runnable> tasks){
super(name);
this.tasks=tasks;
}
@Override
public void run() {
while (tasks.size()>0){
Runnable r = tasks.remove(0);
r.run();
}
}
}
public class MyThreadPool {
private List<Runnable> tasks= Collections.synchronizedList(new LinkedList<>());
private int num;
private int corePoolSize;
private int maxSize;
private int workSize;
public MyThreadPool(int corePoolSize, int maxSize, int workSize) {
this.corePoolSize = corePoolSize;
this.maxSize = maxSize;
this.workSize = workSize;
}
public void submit(Runnable r){
//判断当前集合中任务的数量,是否超出了最大任务数量
if(tasks.size()>=workSize){
System.out.println("任务:"+r+"被丢弃了...");
}else {
tasks.add(r);
//执行任务
execTask(r);
}
}
private void execTask(Runnable r) {
//判断当前线程池中的线程总数量,是否超出了核心数,
if(num < corePoolSize){
new MyWorker("核心线程:"+num,tasks).start();
num++;
}else if(num < maxSize){
new MyWorker("非核心线程:"+num,tasks).start();
num++;
}else {
System.out.println("任务:"+r+" 被缓存了...");
}
}
}
public class MyTest {
public static void main(String[] args) {
MyThreadPool pool=new MyThreadPool(2,4,20);
for(int i=0;i<30;i++){
MyTask myTask=new MyTask(i);
pool.submit(myTask);
}
}
}
@Override
public void run() {
String name =Thread.currentThread().getName();
System.out.println("线程:"+name+"即将执行任务:"+id);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:"+name+"完成了任务:"+id);
}
public MyTask(int id){
this.id=id;
}
}
```java
public class MyWorker extends Thread{
private String name ;
private List<Runnable> tasks;
public MyWorker(String name, List<Runnable> tasks){
super(name);
this.tasks=tasks;
}
@Override
public void run() {
while (tasks.size()>0){
Runnable r = tasks.remove(0);
r.run();
}
}
}
public class MyThreadPool {
private List<Runnable> tasks= Collections.synchronizedList(new LinkedList<>());
private int num;
private int corePoolSize;
private int maxSize;
private int workSize;
public MyThreadPool(int corePoolSize, int maxSize, int workSize) {
this.corePoolSize = corePoolSize;
this.maxSize = maxSize;
this.workSize = workSize;
}
public void submit(Runnable r){
//判断当前集合中任务的数量,是否超出了最大任务数量
if(tasks.size()>=workSize){
System.out.println("任务:"+r+"被丢弃了...");
}else {
tasks.add(r);
//执行任务
execTask(r);
}
}
private void execTask(Runnable r) {
//判断当前线程池中的线程总数量,是否超出了核心数,
if(num < corePoolSize){
new MyWorker("核心线程:"+num,tasks).start();
num++;
}else if(num < maxSize){
new MyWorker("非核心线程:"+num,tasks).start();
num++;
}else {
System.out.println("任务:"+r+" 被缓存了...");
}
}
}
public class MyTest {
public static void main(String[] args) {
MyThreadPool pool=new MyThreadPool(2,4,20);
for(int i=0;i<30;i++){
MyTask myTask=new MyTask(i);
pool.submit(myTask);
}
}
}