java笔记
java笔记第一天
==
和 equals
-
==
比较的比较的是两个变量的值是否相等,对于引用型变量表示的是两个变量在堆中存储的地址是否相同,即栈中存储的引用地址是否相同。 -
equals
方法
1.Object的原生方法比较的是是否指向同一引用对象,即栈中的存储的引用地址是否相等,所以和==
的结果一样。
2.而String,Integer、Date等类重写了equals方法,比较的是当a,b是同一类对象,并且属性值相等,即堆中的内容相等,就返回true,并不一定是同一个对象。
Stirng s1="abc";
String s2=new String("abc");
s1 == s2 ; //false
s1.equals(s2); //true
-
基本数据类型没有
equals
方法,byte,short,char,int,long,float,double,boolean -
Object本身的equals方法也是用双等号(==)进行比较的,所以比较后的结果跟双等号(==)的结果相同,而String,Integer等类重写了equals方法,所以
==
比较的结果不一样。 -
当使用
String str2="abc"
时,程序会首先在字符串池中寻找相同的对象,如果已经有一个str1
创建, 则str2
和str1
的引用相等。而只有引号内包含文本创建对象才会将创建的对象放入到字符串池,而String str=new String("abc")
创建的对象是不放在字符串池中的。
String str1 = "ab";
String str2 = "cd";
String str3 = str1+str2; //这种创建方式是不放入字符串池的,和str4的原理一样
String str4 = str1+"cd"; //这种创建方式是不放入字符串池的。这里实际上创建一个StringBulider对象
然后调用StringBulider.append()方法,然后调用StringBulider.toString()方法。所以和和str7不相等(==)
String str5 = "ab"+str2; //这种创建方式是不放入字符串池的.
String str6 = "ab"+"cd"; //这种创建方式是放入字符串池的,这种情况实际上是创建了1个对象,即"abcd"1个对象,编译阶段会直接合成一个字符串。
String str7 = "abcd";
//str6 与str7都指向了常量池中同一引用地址。
System.out.println(str6==str7); //返回ture
final String fstr="ab";
final String fstr2 = "cd";
String str8=fstr+"cd";
Strins str9 = fstr + fstr2; //此时str9 == str7 也返回true
对于final字段,编译期直接进行了常量替换,而对于非final字段则是在运行期进行赋值处理的,所以str8和
str7 指向常量池中相同位置,所以相等(==)
final修饰的变量,只有定义时指定了初始值,并且被赋值时,只是简单的算术运算和字符串连接,没有访问普通变量,没有调用方法,此时该变量在编译期间就确定值了,类似于宏变量,使用时直接进行常量替换。
下面这种情况就不相等
final String str ; //定义时未赋值,下面在赋值
str = "ab";
String str10 = str + "cd"; //str10 == str7 返回false;str此时不会进行常量替换
如果两个string对象,equals()相等,则他们的hashcode()也相等
String s1="abc"; String ss="abc" //因为java常量池中不会存在两个相同的字符串,所以这两个相同
Sring s2=new String("abc");//或者Sring s2=new String(s1);
此时,s1,s2的堆中的值相等,但栈中的引用值不同,所以(s1==s2)==>false,(s1.equals(s2))==>true
使用new关键字一定会产生一个对象abc,和上面的"abc"不同,同时这个对象存储在堆中。所以上面产生两个对象
一个是存储在栈中的s2,一个是保存在堆中的abc,但是java不存在两个完全一样的字符串对象,所以对象abc
是引用常量字符串中的"abc",即s2-->abc对象--->常量池中的abc(见下方解释)
String s1=new String("abc");
String s2=new String("abc");
//new 出来的两个对象,==比较的是栈中存储的引用地址,肯定不等。
(s1==s2)==>false,(s1.equals(s2))==>true
String s1="abc";
String s2="abc";
(s1==s2)==>true,(s1.equals(s2))==>true
equals() 和hashcode()
- equals()相等的两个对象,hashcode()一定相等;
- equals()不相等的两个对象,hashcode()可能相等,也可能不等。
- 反过来:hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。
- 两个对象相比较,hashcode()相当于字典中索引(比如A),而equals()相当于该索引下的单词,(比如AB,AC)
- 通过字面量赋值创建字符串时,会优先在常量池中查找是否已经存在相同的字符串,倘若已经存在,栈中的引用直接指向该字符串;倘若不存在,则在常量池中生成一个字符串,再将栈中的引用指向该字符串。
- Object的hashcode()方法是根据对象地址计算的(栈地址),所以两个对象的地址不同,hashcode也不同。equals()也比较的是栈地址
- String类的hashcode()方法重写了hashcode(),根据字符串的内容来返回hashcode(),所以相同的字符串有相同的hashcode
System.out.println("abc".hashCode());
System.out.println("abc".hashCode());
System.out.println(new String("abc").hashCode());
System.out.println(new String("abc").hashCode());
//以上四个的hashcode都相等
- 参考下面集合中判断两个对象相等。
String s1 = "abc";
String s2 = new String("abc");
s2 = s2.intern(); 此时`s1==s2`返回true,
`intern()`会先检查字符串池,如果存在,就返回池里的字符串的引用;如果不存在,该方法会 把"abc"添加到字符串池中,然后再返回它的引用。
当且仅当s1.equals(s2)返回true时, s1.intern()==s2.intern()才返回true
-
String str=new String("abc")
创建了两个对象,String的构造器:public String(String original) { //other code ... } ,我们正是使用new调用了String类的上面那个构造器方法创建了一个对象,并将它的引用赋值给了str变量。同时我们注意到,被调用的构造器方法接受的参数也是一个String对象,这个对象正是"abc"。一个在常量池中,一个在堆中。
String对象
字符串常用方法
-
charAt()
----返回指定索引处的字符串 -
concat()
--将一个字符串追加到另一个字符串的末尾 -
equalseIgnoseCase()
判断两个字符串的相等性,忽略大小写 -
length()
返回字符串中的字符个数 -
replace()
用新字符代替指定的字符 -
substring()
返回字符串的一部分 -
toLowerCase()
将字符串中的大写字符转换成小写字符返回 -
toString()
返回字符串的值 -
toUpperCase()
将字符串中的小写字符转换成大写字符返回 -
trim()
删除字符串前后的空格 -
splite()
将字符串按照指定的规则拆分成字符串数组 -
toCharArray()
字符串转换为一个字符数组 -
indexOf()
返回字符串第一次出现的索引
//字符串和字符数组互相转换
String str1 = "hello" ; // 定义字符串
char c[] = str1.toCharArray() ; // 将一个字符串变为字符数组
String str2 = new String(c) ; // 将全部的字符数组变为String
String str3 = new String(c,0,3) ; // 将部分字符数组变为String
- valueOf() 基本类型转换为字符串
valueOf 是静态方法,源码如下:
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
所以使用obj.toString()方法必须不为null,valueOf可以使用null,但返回的是字符串"null"
- s1.compareTo(s2),按照字典顺序比较两个字符串 区分大小写;compareToIgnoreCase不区分大小写
s1.compareTo(s2) 相等时返回 0
不想等是有两种情况 长度不同和对应索引处字符不同
1. 对应索引的值不同时,则返回此索引处的ascii码差值 即charAt(k) - charAt(k)
2. 长度不同时,则短字符是长字符的开头,返回长度差 即length() - length()
- Object toString()
- String 与其他类型数据进行连接操作时(+),将自动调用该对象类的toString()方法。
- Object 类的toString()默认返回的是一个类的类名+"@"+此对象的hashcode(16进制)。
获取系统当前时间 的毫秒数
- long time = System.currentTimeMillis();
StringBuffer (线程安全)
- StringBuffer 对其自身修改时,不会创建新的字符串对象(Sring对象修改,都会创建一个新的对象)
- StringBuffer 不能使用"="初始化,必须创建实例初始化
- 主要操作为append() 和 insert(offset,str),reverse()
StringBuilder (线程不安全) (StringBuffer和Stringbuilder内部使用的是字符串数组存储)
- 比StringBuffer速度快,也常用于append() 和 insert(),reverse()操作
字符串拼接 +, concat, append
- 速度最慢
虽然编译器对+进行了优化,它是使用StringBuilder的append()方法来进行处理的,但是使用append后使用了
toString(),即 str+='b',等价于 str=new StringBuilder(str).append(b).toString(),所以变慢的原因是
new StringBuilder()和 toString()
- concat() 速度次之
源码中最后使用了return new String() ,所以变慢
- append()速度最快
最后返回的是本身,没有创建新的对象
判断字符串是否为空
null不是对象,""空字符串是对象,所以null没有空间,空字符串有空间
equals()用于比较对象,而null不是对象,所以null用等号=
所以当 str=null,下面比较是错误的
if(str.equals("")||str==null){} //null不能调用equals方法,所以会报空指针异常
正确的为if(str==null ||str.equals(""))
所以判断字符串为空,首先判断是否为null,所以高效的写法为
if(str==null || str.length()<=0){}
不为空
if(str1!=null || str.length()!=0){}
集合 Java的容器类主要由两个接口派生而出:Collection和Map。集合中的元素类型都是Object
- List 关注事物的索引列表,有序集合
- Set 关注事物的唯一性,无序集合
- Quene 关注事物被处理时的顺序
- Map 关注事物的映射和键值的唯一性
Collection接口 Collection是容器层次结构中根接口。而Collections是一个提供一些处理容器类静态方法的类
Collection接口是 Set 、List 和 Queue 接口的父接口,提供了多数集合常用的方法声明,Iterator是Collection的父接口
AbstractCollection实现了Collection接口, AbstractList和AbstractSet都继承于AbstractCollection,并且AbstractList实现了List接口
AbstractSet实现了Set接口。具体的List实现类继承于AbstractList,而Set的实现类则继承于AbstractSet
-
add(E e)
将指定对象添加到集合中,addAll(Collection c)
将指定集合的所有元素添加进来 -
remove(Object o)
将指定的对象从集合中移除,移除成功返回true,不成功返回false -
contains(Object o)
查看该集合中是否包含指定的对象,包含返回true,不包含返回flase -
size()
返回集合中存放的对象的个数。返回值为int -
clear()
移除该集合中的所有对象,清空该集合。 -
iterator()
返回一个包含所有对象的iterator对象,用来循环遍历 -
toArray()
返回一个包含所有对象的数组,类型是Object -
toArray(T[] t)
返回一个包含所有对象的指定类型的数组 -
isEmpty()
判断集合是否有元素
Collections 工具类 提供的一些方法
- void reverse(List list) //反转元素
- void shuffle(List list) //随机排序
- void sort(List list) //升序排序
- int binarySearch(List list,Object key) //二分查找
- Object max(),min() //排序后的最大值
将集合中不安全的集合包装成线程同步的集合
Collection c=Collections.synchronizedCollection(new ArrayList());
List list=Collections.synchronizedList(new ArrayList());
Set s=Collections.synchronizedSet(new HashSet());
Map m=Collections.synchronizedMap(new HashMap());
Iterator接口,Iterator是Collection的父接口,ListIterator专门用于遍历List
- 常用方法
- boolean hasNext() //是否存在访问的元素
- next() //访问下一个元素
- void remove() //删除上次访问的元素,必须先访问后执行,如果没调用next(),则不合法
- 迭代器访问Collction集合元素时,集合里的元素不能被改变,只是把值传给了迭代变量
- 正确的顺序
Iterator it=c.iterator();
while(it.hasNext()){
it.next();
it.remove();
}
- ListIterator 可以向前和向后遍历
void add(E e) //将元素插入List
boolean hasPrevious()
E previous() //返回前一个元素
List接口
List特有方法
List 一个有序的序列,元素可以重复,每一个元素都有一个索引,关心的是索引,与其他集合相比,List特有的就是和索引相关的一些方法:get(int index) 、
set(index,Object elem) 用elem替换指定位置元素,并返回旧元素
add(int index,Object o) 、 indexOf(Object o)第一次出现元素o的位置。
List判断对象相等的标准是 只要通过equals()方法返回true即可
- List接口实现类
- ArrayList 可以将它理解成一个可增长的数组(初始容量是10),顺序存储,它提供快速迭代和快速随机访问的能力。线程不安全,可以存null
继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。
有个数组成员变量 Object[] elementData 用于保存ArrayList中的元素,当容量超过时,设置新的容量为 (原始容量*3)/2+1
E set() //设置位置上的元素,并且返回旧元素
void trimToSize() //将容量设置为实际元素的个数
void clear() //清空ArrayList,将全部的元素设为null
Object clone() //克隆ArrayList,内部是将ArrayList拷贝到一个新的ArrayList中,并返回
//源码
// 将e添加到ArrayList的指定位置
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: "+size);
ensureCapacity(size+1); // Increments modCount!!
//将数组容量增大一位,然后等价于从index开始数组元素每个后移一位
System.arraycopy(elementData, index, elementData, index + 1,size - index);
elementData[index] = element; //然后设置index位置新值
size++;
}
ArrayList遍历
- Iterator遍历
Iterator iter=list.iterator();
while(iter.hasNext()){
iter.next();
}
- 通过索引值随机访问
int size=list.size();
for(int i=0;i<size;++i){
list.get(i);
}
- foreach
for(Interger i : list){
value=i;
}
ConcurrentModificationException异常 当遍历List的时候同时对其修改(增、删),就会抛出此异常
- ArrayList的iterator()方法是在父类AbstractList中实现的,而AbstractList的iterator()方法,返回一个Itr类对象,此类是AbstractList的成员内部类,实现了Iterator接口。
public class Test {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(2);
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
Integer integer = iterator.next();
if(integer==2)
list.remove(integer);
}
}
}
Itr类的成员变量
1. cursor //表示下一个要访问的元素的索引
2. lastRet //上一个访问元素的索引
3. exceptedModcount //表示对ArrayList修改的次数的期望值,初始值为modCount,
modCount是AbstactList的成员变量,ArrayList每次调用add()和remove()时,都会对modCount进行加1
//Iterator的hasNext()方法
public boolean hasNext(){
return cursor !=size(); //判断下一个访问元素的下标是否等于ArrayList大小,不等则还有元素访问。
}
//Iterator的next()
public E next() {
checkForComodification();
try {
E next = get(cursor); //通过cursor获得下一个要访问元素的下标
lastRet = cursor++; //把cursor赋值给lastRet,然后自增,初始时cursor=0,lastRet=-1,调用一次后cursor=1,lastRet=0
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification(); //此方法会判断modCount和exceptedModCount是否相等,不相等则抛出异常,
//此时modCount=0,exceptedModCount=0
throw new NoSuchElementException();
}
}
然后调用ArrayList的remove(),此方法会对modCount加1,size减1,对以上测试程序,对于iterator,exceptedModcount=0,cursor=1,lastRet=0
对于list ,modCount=1,size=0。然后循环再调用hahNext(),此时cursor=1,size=0,所以不相等,继续执行。而再调用iterator的next,会调用
checkForComodification(),此时不相等,就抛出异常了。即调用list.remove()会导致modCount与exceptedModCount不一致。
而迭代器同样有remove()方法,虽然此方法内部调用的list的remove,但是多了一个exceptedModCount=modCount,所以不会报错。所以迭代器中删除元素
需要调用iter.remove()。
fail-fast原理
-
上述即使使用迭代器的remove()方法,在多线程中,当一个线程遍历,另一线程修改时,也会导致exceptedModCount和modCount不一致,抛出异常。
解决方法为加锁,或者使用CopyOnWriteArrayList -
当多个线程对同一个集合进行操作的时候,某线程访问集合的过程中,该集合的内容被其他线程所改变即其它线程通过add、remove、clear等方法,改变了modCount的值;这时,就会抛出ConcurrentModificationException异常,产生fail-fast事件
ArrayList 类似于顺序表,适合随机存取,set和get;而 LinkedList 类似于链表,适合插入和删除,add和remove,只需移动指针即可。
- LinkedList 中的元素之间是双链接的,当需要快速插入和删除时LinkedList成为List中的不二选择,元素可以为null。
-
LinkedList随机访问时,必须从首节点(或尾节点,索引小于列表长度一半时从队首开始遍历,反之从队尾)
-
LinkedList 继承于AbstractSequentialList类,AbstractSequentialList继承与AbstractList。实现了List,Deque等接口
AbstractSequentialList 实现了随机访问List的一些方法。get(int index)、set(int index,E element)、add(int index,E element)
和remove(int index)
LinkedList本质是双向循环链表 包含两个成员变量 header和size
header是双向链表的表头,它是双向链表节点所对应的类Entry的实例。Entry是双向循环链表数据结构,Entry包含成员变量 previous,next,element。
previous是当前节点的上一个节点,next是当前节点的下一个节点,element是当前节点所包含的值。
List遍历
- foreach 遍历
- Iterator 遍历
- 下标遍历 list.size()
使用foreach结构的类对象必须实现了Iterable接口
Vector线程安全,因为都是同步方法
- Stack()是vector的子类
Set接口(内部是map实现的) ,三个实现类都是线程不安全的
Set关心唯一性,它不允许重复。
HashSet 当不希望集合中有重复值,并且不关心元素之间的顺序时可以使用此类。(内部是hashmap)
HashSet不是线程同步的。集合元素可以是null。HashSet判断两个元素相等的标准是equals()相等,并且
hashcode()返回值也相等。这两个有一个不相等,即是不重复。
如果两个对象的hashCode()返回值相等,但是equals()返回false,因为两个对象的hashcode值相同,HashSet将试图将他们保存在同一个位置,只能用链式结构保存对象,但是HashSet是根据hashcode定位元素,这样的话就会导致性能下降
HashSet内部有一个HashMap的成员变量,所有操作都是基于这个HashMap操作的。HashSet存储的实际是HashMap的key,所以不允许有重复元素,元素可以为null。
HashSet内部使用一个静态常量Object类变量PRESENT,作为HashMap的value,所以HashSet中存储的key的value值都为PRESENT
add(E e)方法
public boolean add(E e){
return map.put(e,PRESENT) == null ;
}
添加成功返回true,否则为false
因为HashMap的put方法时,若已存在key,会用新value替换旧的value,但是key不会改变,并返回旧value,此时HashSet的add()方法便返回FALSE
而没有该key时,便添加该key,并返回null,而此时的add()返回true,添加成功
contains()方法
public boolean contains(Object o) {
return map.containsKey(o); //调用HashMap的containsKey
}
遍历方法
1. 迭代器遍历
for(Iterator iter=set.iterator();iter.hasNext();){
iter.next();
}
2.toArray()转换为数组遍历
String[] strArr=(String[])set.toArray(new String[0]);
for(String str:strArr){
syso(str);
}
LinkedHashset 当不希望集合中有重复值,并且希望按照元素的插入顺序,有序进行迭代遍历时可采用此类。
TreeSet 使用树结构实现(红黑树)当不希望集合中有重复值,并且希望按照元素的自然顺序进行排序时可以采用此类。(自然顺序意思是某种和插入顺序无关,而是和元素本身的内容和特质有关的排序方式,譬如“abc”排在“abd”前面。)TreeSet判断对象相等标准是 两个对象通过compareTo(Object obj)方法比较是否返回0(内部是TreeMap)
Queue接口
Queue接口继承Collection接口,用于保存将要执行的任务列表。一种队列则是双端队列,支持在头、尾两端插入和移除元素,主要包括:ArrayDeque、LinkedBlockingDeque、LinkedList。另一种是阻塞式队列,队列满了以后再插入元素则会抛出异常,主要包括ArrayBlockQueue、PriorityBlockingQueue、LinkedBlockingQueue。
LinkedList 同样实现了Queue接口,可以实现先进先出的队列。
PriorityQueue 用来创建自然排序的优先级队列。
Deque<E>接口
Deque接口是双端队列接口,继承自Queue接口。可以再两头插入和删除,所以相比Quene多了一些在两端操作的方法。比如addFirst(),addLast(),peekLast(),pollLast()等等
LinkedList 同样实现了Deque的接口
Map接口
Map是一个键值对的集合。也就是说,一个映射不能包含重复的键,每个键最多映射到一个值。关心的是唯一的标识符。他将唯一的键映射到某个元素。当然键和值都是对象。键不能重复,值可以重复。
HashMap,TreeMap继承于AbstractMap,AbstractMap实现了Map接口,Hashtable虽然继承于Dictionary,但实现了Map接口
HashMap 当需要键值对表示,又不关心顺序时可采用HashMap,HashMap中元素的排列顺序不固定,HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
Hashtable 注意Hashtable中的t是小写的,它是HashMap的线程安全版本,现在已经很少使用。
LinkedHashMap 是HashMap的子类,定义了当前对象的上一个和下一个引用,在hash表基础上形成双向循环链表,当需要键值对,并且关心插入顺序时可采用它。
LinkeedHashMap默认采用的按照put()顺序排序,新添加元素都会添加指链表末尾。
public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder)此构造方法可以设置按照访问顺序排序,即最近访问的会
从原位置删除,添加至链表末尾。所以第一个节点是最少访问的节点。
LinkedHashMap采用双向循环链表将所有Entry连接起来,这样遍历时,就不用循环table数组,只要遍历循环链表即可。
1.添加元素
LinkedHashMap并没有重写HashMap的put方法,而是重写了若key存在时调用的recordAccess方法,和key不存在直接插入的addEntry()和createEntry()方法。当按照访问顺序排序时,这几个重写方法会使最近访问的元素移至链表末尾。所以就算不使用get(),再次put()已经put()过的元素,也会使顺序改变。并且如果实现LRU算法时,要重写removeEldestEntry()方法,该方法在addEntry()中被调用,意味着超过一定大小时,就将最近未访问的节点删除掉,即第一个节点
2. 访问元素
LinkedHashMap重写了get()方法,调用该方法时,如果是按照访问顺序排序,也会调用recordAccess()方法,也会将元素移至链表末尾,改变顺序。
3. LinkedHashMap实现LRU
首先,当accessOrder为true时,才会开启按访问顺序排序的模式,才能用来实现LRU算法。我们可以看到,无论是put方法还是get方法,都会导致目标Entry成为最近访问的Entry,因此便把该Entry加入到了双向链表的末尾(get方法通过调用recordAccess方法来实现,put方法在覆盖已有key的情况下,也是通过调用recordAccess方法来实现,在插入新的Entry时,则是通过createEntry中的addBefore方法来实现),这样便把最近使用了的Entry放入到了双向链表的后面,多次操作后,双向链表前面的Entry便是最近没有使用的,这样当节点个数满的时候,删除的最前面的Entry(head后面的那个Entry)便是最近最少使用的Entry。
TreeMap 当需要键值对,并关心元素的自然排序时可采用它,继承于AbstractMap,且实现了NavigableMap接口因此,TreeMap中的内容是“有序的键值对。
在Map 中插入、删除和定位元素,HashMap是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。TreeMap的key必须实现Comparable接口,并且key是同一类的对象
Map接口的常用方法如下表所示:
put(K key, V value)
向集合中添加指定的键值对,若已存在则覆盖,并返回旧值,若键值之前不存在,则返回null
putAll(Map t)
把一个Map中的所有键值对添加到该集合
containsKey(Object key)
如果包含该键,则返回true
containsValue(Object value)
如果包含该值,则返回true,通过判断equals()返回true
get(Object key)
根据键,返回相应的值对象
keySet()
将该集合中的所有键以Set集合形式返回
values()
将该集合中所有的值以Collection形式返回
remove(Object key)
如果存在指定的键,则移除该键值对,返回键所对应的值,如果不存在则返回null
clear()
移除Map中的所有键值对,或者说就是清空集合
isEmpty()
查看Map中是否存在键值对
size()
查看集合中包含键值对的个数,返回int类型
Set entrySet()
返回键值对视图
entrySet
返回key-value的Set<Map.Entry<k,v>>集合
keySet
返回key的Set集合
values
返回value的Collection集合
HashMap
- HashMap key 和 value都可以为null
- HashMap有一个Entry的内部类,是hash表的数据结构,用来存储key-value对
- HashMap 有一个叫做table的Entry数组的成员变量,数组的索引作为hash插在的索引值,并且指向了存储key-value的链表
- key的hashcode()方法用来寻找Entry对象所在的bucket
- 如果两个key有相同的hash值,则他们会放在一个桶里,并且按照头插法插入链表
- key的equal()方法来判断两个就算key相同,是否为同一元素,从而确保key的唯一性
- value对象的hashcode()和equals()并没有什么作用
//成员变量
1. table 一个Entry[]数组,Entry是一个单向链表,哈希表的key-value对都存储在Entry数组中
2. size HashMap的大小,保存key-value键值对的数量
3. threshold 用于判断是否需要调整HashMap的容量。threshold的值="容量*加载因子",当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
4. loadFactor就是加载因子。
5. modCount是用来实现fail-fast机制的。
构造函数
- HashMap中有两个重要的参数,Capacity和负载因子(Load factor),Capacity就是table的大小,默认为16(2的倍数);loadFactor默认值为0.75;
还有一个阈值(threshold),等于Capacity * loadFactor,所以默认为12,当存储容量超过阈值,就调整table大小为当前大小的2倍
HashMap有4个默认构造函数,此时会初始化table数组
1.
// 默认构造函数。
public HashMap() {
}
2.
// 指定“容量大小”和“加载因子”的构造函数
public HashMap(int initialCapacity, float loadFactor) {
//最大容量为2^30,超过这个容量,将被这个最大值替换。并且容量必须是2的倍数,就算不是,也会扩容至2的倍数
//比如设置初始值为17,会扩容32;初始值为15,会扩容16
3. public HashMap(Map<? extends K, ? extends V> m) {
// 使用迭代法,将形参的元素加入到HashMap中
for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
Map.Entry<? extends K, ? extends V> e = i.next();
putForCreate(e.getKey(), e.getValue());
}
}
put(key,value)方法
1. 若key为null,则调用putForNullkey(value),遍历table,若存在key为null,则用 新value 替换 旧value,返回旧value。否则key为null的hash
值为0,存放再table[0]中,返回
2. 若key不为null,则计算key的hash值,通过hash值得到key在table中的索引
3. 遍历此索引下table[index]的链表,如果发现此链表中存在 键值的hash值和key的hash值相等,并且 key值相等(==比较相等,即栈地址相等) 或者key.equals(k)返回true,
即已存在key对应的值,则用value替换旧value,并返回旧value,保证key的唯一性
4. 若不存在key对应的值,在插入之前调用addEntry()先判断HashMap中size大小,如果size大于threshold,则扩容table.length*2两倍。
然后调用createEntry(),新建一个key-value节点,按照头插法插入链表。然后返回null
put()时涉及到的问题 hash算法 扩容
- hash算法
1. 先调用hash()方法,此方法根据key的hashcode进行二次hash,防止hash冲突
2. 根据hash()得到的hash值,然后调用indexFor()计算在table中的索引。
static int indexFor(int h, int length) {
return h & (length-1);
}
此处的 h & (length-1) 等价于 h % length,但是 & 比 % 更高效。因为length总是等于2^n。
因为偶数的最低位为0,奇数的最低位为1。假设length不是偶数,假设为15,length-1=14;这样在和14进行&运算的时候,最低位永远是0,那么0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的,空间减少,进一步增加碰撞几率,这样就会导致查询速度慢
而当lenth为偶数,假设为16,length-1=15=1111; 2^n-1 得到的数二进制低位全为1, 在进行&时,得到的值总是和原来的hash值相同,这样的话只有hash
值相同的,才会放入同一个链表中,不同的hash值发生碰撞的几率较小,提高查询效率。
所以对于任意对象,只要它的hashCode()值相等,则hash()方法计算出来的hash码值也相等,而在计算索引时,hash值相等的会存入同一个链表。
所以当两个对象(key)的hashCode()值相等,它们就会被存入同一个链表中。
- 扩容(rehash)
1.8以下版本,1.8加入了红黑树
1. 调用resize(newCapacity),传入新的容量,然后会初始化一个新的大Entry数组,Entry[] newTable = new Entry[newCapacity];
2. 调用transfer(newTable)方法,传入新数组。遍历旧的table数组,加入新的数组中
for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
Entry<K,V> e = src[j]; //取得旧Entry数组的每个元素
if (e != null) {
src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
e.next = newTable[i]; //头插法插入新的链表
newTable[i] = e; //将元素放在数组上
e = next; //访问下一个Entry链上的元素
} while (e != null);
}
}
因为重新放入新数组中也是使用头插法,所以之前顺序为s1->s2->s3,扩容之后就为s3->s2->s1
3. 再将table指向新的newTable,修改阈值
//扩容的过程中,在多线程中链表出现循环链,可能造成死循环,详见印象笔记
get(key)方法
1. 若key为null,调用getForNullKey()方法,在table[0]处查找,找到则返回value,否则返回null
2. 若key不为null,则调用hash(key.hashcode())得到key的hash码,然后调用indexFor(),计算在table中的索引,然后遍历索引下的单链表,
若有元素和key的hash值相同,并且key值相等或者key.equals(k)返回true,则找到,返回value,否则返回null
remove(key) 删除键值为key的元素
1. 调用removeEntryForKey(key),若key为null,则key的hash值为0,否则调用hash()计算hash码,然后调用indexFor()计算索引,遍历该索引下的
链表。若为第一个元素,则table[i]指向下一个元素,否则就是执行单链表的删除,并返回该Entry对象。
2. 若该Entry对象为null,则未找到该元素,返回null,否则返回删除的value
使用自定义对象作为HashMap的key,一定要重写hashCode()和equals(),否则该key在外部被改变,hashCode()即被改变,再get()会返回null
1. 重写hashCode()是为了对同一个key,能得到相同的hashCode(),就可以定位到相应的key上
2. 重写equals()是为了向HashMap表明当前对象和key上所保存的对象是相等的,这样才真正地获得了这个key所对应的这个键值对。
containsKey(key)
1. public boolean containsKey(Object key) {
return getEntry(key) != null;
}
2. 调用getEntry(),getEntry()作用就是返回返回“键为key”的键值对。HashMap将“key为null”的元素存储在table[0]位置,“key不为null”的则调用hash()计算哈希值,然后计算索引,如果存在key对应的元素,则返回该Entry对象,否则返回null
containsValue(value)
1. 若value为null,则调用containsNullValue(),containsNullValue()两层循环每个索引下的链表,判断是否有value=null的,若有则返回true,否则返回null。
2. 若value不为null,也是双重循环遍历,判断value.equals(e.value),若有则返回true,否则返回null。
Set<Map.Entry<k,v>> entryset values() keySet()三者类似
1. 内部有一个实现Iterator接口的类HashIterator(),当我们通过entrySet()获取到的Iterator的next()方法去遍历HashMap时,实际上调用的是 此类的nextEntry()方法。而nextEntry()的实现方式,先遍历Entry(根据Entry在table中的序号,从小到大的遍历);然后对每个Entry(即每个单向链表)逐个遍历
Map遍历
Map遍历有总计四种方式,方法有两种:(Entry是Map的静态内部类)
- 一类是基于map的Entry;map.entrySet();
- 一类是基于map的key;map.keySet()
访问方式也有两种: - 利用迭代器Iterator
- 利用foreach循环
- Map的内部类Entry,Entry包含如下三个方法
- K getKey() //返回Entry里包含的key值
- V getValue() //返回Entry里包含的value值
HashMap和Hashtbale区别
- Hashtable是同步方法,所以是线程安全的;HashMap不是线程安全的
- HashMap的可以有一个key为null,value为多个null;而hashtable不允许key和value为null,否则会抛出NullPointerException
- HashMap继承于AbstractMap,AbstractMap实现了Map接口,Hashtable虽然继承于Dictionary,但实现了Map接口
- HashMap的table数组初始容量是16,Hashtable初始容量是11,填充因子默认都是0.75;HashMap扩容是当前容量的2倍,Hashtable扩容是capaciy*2+1
- Hash计算方式不同,Hashtable是直接对table的length求余(hash&0x7fffffff)%length;HashMap是 h & length-1
集合中判断两个对象相等
- 判断两个对象的hashCode是否相等
如果不相等,认为两个对象也不相等,完毕
如果相等,转入2)
(这一点只是为了提高存储效率而要求的,其实理论上没有也可以,但如果没有,实际使用时效率会大大降低,所以我们这里将其做为必需的。) - 判断两个对象用equals运算是否相等
如果不相等,认为两个对象也不相等
如果相等,认为两个对象相等(equals()是判断两个对象是否相等的关键)
hashCode方法的存在是为了减少equals方法的调用次数,从而提高程序效率
Comparable 与 Comparator
-
Comparator位于包java.util下,而Comparable位于包java.lang下
-
Comparator的重写方法为public int compare(T1,T2){}; Comparable的重写方法为 public int compareTo(Object o){}
-
若要对一个类或者集合排序,就要实现Comparable接口,重写compareTo()方法,该类就成为可以比较的类。或者在外部实现Comparator接口,重写compare
方法,这样就不用修改原类的代码,就可以比较了。 -
如 String、Integer 自己就可以完成比较大小操作,已经实现了Comparable接口
-
当调用数组排序 Arrays.sort(Object[] arr)时,默认调用的就是compareTo()方法,所以该类必须实现Comparable接口,而
Arrays.sort(T [],Comparator)。就必须传入一个实现Comparator接口的对象 -
同样的Collections.sort(List<T> ) ,List中的元素必须实现Comparable接口;Collections.sort(List<T>,Comparator)就必须传入一个实现Comparator接口的对象,然后即可对list进行排序
HashMap按照value排序
- TreeMap可以实现按照key排序,当想使用value排序时,就先转换为List,然后调用Collections.sort()排序
Map<String,Interger> map = new HashMap<>();
List<Map.entry<String,Interger>> li = new ArrayList<>(map.entrySet());
Collections.sort(l, new Comparator<Map.Entry<String, Integer>>() {
public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
return (o2.getValue() - o1.getValue());
}
});
数组
- System.arraycopy(srcArray,srcPos,destArray,desPos,int length)数组拷贝
srcArray - 源数组。
srcPos - 源数组中的起始位置。
destArray - 目标数组。
destPos - 目标数据中的起始位置。
length - 要复制的数组元素的数量。
- Arrays.copyOf() //拷贝数组
int []a={1,2,3,4,5};
int [] b=Arrays.copyOf(a,a.length);
如果第二个变量大于源数组的长度,则超出值默认为0
- Arrays.asList()
Arrays.asList() 返回一个List,但是该List不支持add和remove操作,其内部返回一个ArrayList对象
但是此ArrayList并不是 java.util.ArrayList, 而是Arrays的内部类。
Arrays.asList()最好传递的是引用类型,并且使用此方法后,数组就和链表链接在一起,修改其中
任何一个,另外一个也会变化。
栈
- public Stack( ) 创建一个空栈,只有这个构造函数,Stack继承Vector,Vector继承AbstractList
Stack<Interger> sta=new Stack<Interger>();
- 常用方法
1. public boolean empty() //栈是否为空,因为栈实现了Collection接口,所以可用isEmpty()
2. public E pop() //出栈
3. public E push(E item) //进栈
4. public E peek() //查看栈顶元素,但不移除
5. int search(Object o ) //返回对象在栈中的位置,以1为基数
java正则表达式
- String 类中的正则方法
1. boolean matches(reger) //验证字符串是否匹配
str.matches("\/w+");
2. String replaceAll(regex,replacement) //用replacement替换全部匹配的字符,replace没有正则参数
//去掉前后空格方法一
String regex = "^\\[(.*)\\]$";
String s1 = str.replaceAll(regex, "$1");
//方法二,注:replace方法无正则匹配
String regex = "^\\[|\\]$";
String s1 = str.replaceAll(regex, "");
3. String[] split() //根据正则才拆分字符串,结果为一个字符串数组
String str = "asfasf.sdfsaf.sdfsdfas.asdfasfdasfd.wrqwrwqer.asfsafasf.safgfdgdsg";
String[] strs = str.split("\\.");
for (String s : strs){
System.out.println(s);
}
- 正则类
- Pattern 没有公共构造方法,必须调用静态方法 Pattern.complie(pattern)创建对象
- Mathcher 返回正则匹配的结果,通常用于匹配多个结果
Pattern主要方法
- Pattern.matches(regex,string) 等价于str.matches(regex),也等价于matcher对象的matches()方法,(没有参数)。
boolean matches = Pattern.matches(pattern, text); //静态方法
- 对象方法 split(text)
String patternString = "sep";
Pattern pattern = Pattern.compile(patternString);
String[] split = pattern.split(text);
System.out.println("split.length = " + split.length);
for(String element : split){
System.out.println("element = " + element);
}
Mathcher用法
- 创建Mathcher对象
String text = "This order was placed for QT3000! OK?";
String pattern = "(.*)(\\d+)(.*)";
// 创建 Pattern 对象
Pattern pat = Pattern.compile(pattern);
// 现在创建 matcher 对象
Matcher mat = pat.matcher(text);
- 方法用例
1. boolean matches = mat.matches(); //检测结果是否匹配,等同于以上两个方法
2. 查找所有匹配到的结果
String text =
"This is the text which is to be searched " +
"for occurrences of the word 'is'.";
String patternString = "is";
Pattern pattern = Pattern.compile(patternString); //先创建Pattern对象
Matcher matcher = pattern.matcher(text); //用Pattern对象方法创建Matcher对象
int count = 0;
while(matcher.find()) {
count++;
System.out.println("found: " + count + " : " + matcher.start() + " - " + matcher.end());
}
find() 方法返回第一个是否匹配,之后每次调用 find() 都会返回下一个。
start()和end()返回每次匹配的字串在整个文本中的开始和结束位置。end返回的是字符串末尾的后一位,多一位
3. group获得分组结果
String text = "John writes about this, and John writes about that," +
" and John writes about everything. " ;
String patternString1 = "(John)";
Pattern pattern = Pattern.compile(patternString1);
Matcher matcher = pattern.matcher(text);
while(matcher.find()) {
System.out.println("found: " + matcher.group()); //因为只有一个分组
}
//多个分组
String text =
"John writes about this, and John Doe writes about that," +
" and John Wayne writes about everything."
;
String patternString1 = "(John) (.+?) ";
Pattern pattern = Pattern.compile(patternString1);
Matcher matcher = pattern.matcher(text);
while(matcher.find()) {
System.out.println("found: " + matcher.group(1) +
" " + matcher.group(2));
}
3. 替换,等价于String类的replaceAll()方法
replaceAll(replacement) 和 replaceFirst() 方法可以用于替换Matcher搜索字符串中的一部分。replaceAll() 方法替换全部匹配的正则表达式,replaceFirst() 只替换第一个匹配的。
在处理之前,Matcher 会先重置。所以这里的匹配表达式从文本开头开始计算。即后面继续替换,也是从头开始
String text =
"John writes about this, and John Doe writes about that," +
" and John Wayne writes about everything."
;
String patternString1 = "((John) (.+?)) ";
Pattern pattern = Pattern.compile(patternString1);
Matcher matcher = pattern.matcher(text);
String replaceAll = matcher.replaceAll("Joe Blocks ");
System.out.println("replaceAll = " + replaceAll);
- str.matches()和Pattern.matches()以及mat.matches() 只会当字符串全部匹配的时候,才返回true
String text="abcd";
String pattern="abc" ,此时使用以上三个方法,都为false,因为不是完全全部匹配
-
mat.find() 可以在随意位置匹配,但是如果之前已经使用过find(),需要用mat.reset()重置匹配
-
mat.group() 返回匹配到的子字符串,等价于 str.substring(mat.start(),mat.end())
while(m.find()) {
System.out.println(m.group()); //m.group()等价于m.group(0)返回整个匹配结果
System.out.print("start:"+m.start());
System.out.println(" end:"+m.end());
}
- mat.group(int i) 专用于分组操作,用于返回分组结果,即正则中()中匹配的结果,mat.groupCount()用于返回有多少组,即多少个(),无分组的话返回0
String text="BBC ABCDAB ABCDABCDABDE";
String pattern="(AB)CD";
while(mat.find()) {
System.out.println(mat.groupCount()); //返回1
System.out.println(mat.group(1));
System.out.println("found: " + count + " : " + mat.start() + "-" + mat.end());
}
mat.group()会返回 所有ABCD
mat.group(1)返回所有AB
try..catch
- try块是必须的,catch块和finally块是可选的,但是必须至少出现一个,可以有多个catch块。一个catch块可以捕获多个异常,用|分割
- 通常不要在finally块中使用return,throw,一旦使用了,将会导致try块,catch块中的return、throw语句失效。当try块,catch中遇到return,throw时
程序不会结束,而是查看是否有finally块,若有,则执行finally块中代码,然后再返回try块,catch执行return,throw。
若finally块中也有return,throw,则程序结束,不会再返回try块,catch执行 - 所以不管try块,catch执行什么的代码,finally块总会被执行。除非try块,catch调用了退出虚拟机的方法(System.exit(1))
异常
-
java的异常父类为Throwable,继承的有两大类Error,Exception。Error程序无法处理和捕获,Exception又分为IOException和RuntimeException
-
IOException必须try..catch,而RuntimeException(运行时异常)包括常见的空指针,数组越界,类型转换异常,无需try..catch
-
异常捕获时先捕获小异常,再捕获大异常。
-
throws抛出异常时,当前方法不知如何处理,然后就交给上一级处理,如果main方法也不知如何处理,也throws抛出异常,然后交给JVM处理
throws用来抛出异常类,用在方法上 void test() throws IOException{}。throws可以抛出多个异常类,用,分开
使用throws抛出异常就无需try..catch。然后一个方法throws异常,则调用该方法时,要么放在try..catch中,要么放在另一个throws方法中。
当有子类方法重写父类的throws异常时,子类抛出的异常必须是父类抛出异常的子类或者相同
- throw程序自行抛出异常,throw抛出的是异常实例,放在方法内
void test(){
throw new IOException();
}
如果throw抛出的异常,不是RuntimeException异常,则该throw语句必须要么处在try块里,显示捕获异常;要么放在一个带throws方法里,由调用者处理。
面向对象
- 类的初始化过程
-
初始化
父类
的静态变量和静态代码块(也叫类初始化块) -
初始化
子类
的静态变量和静态代码块 -
初始化
父类
的普通成员变量和普通代码块,再执行父类
的构造方法 -
初始化
子类
的普通成员变量和普通代码块,再执行子类
的构造方法 -
初始化块经过编译实际上会被还原到每个构造器中,且位于构造器代码的最前面
-
普通话初始化块负责对对象进行初始化,静态初始化块负责对类进行初始化
-
只有在第一次创建对象的时候才会创建静态初始化块,此后会一直存在,所以后面创建对象时无需再类初始化
-
而普通初始化块,每次创建对象都会执行。等同于构造方法
class Father{
int i=5;
// 静态变量
public static String p_StaticField = "父类--静态变量";
// 变量
public String p_Field = "父类--变量";
// 静态初始化块
static {
System.out.println(p_StaticField);
System.out.println("父类--静态初始化块");
}
// 初始化块
{
System.out.println(p_Field);
System.out.println("父类--初始化块");
}
//父类构造方法
Father(){
System.out.println("Father"+this.i);
test();
}
public void test(){
System.out.println("father test"+this.i);
}
}
public class Test extends Father {
int i=50;
// 静态变量
public static String staticField = "子类静态变量";
// 变量
public String field = "子类变量";
// 静态初始化块
static {
System.out.println(staticField);
System.out.println("子类静态初始化块");
}
// 初始化块
{
System.out.println(field);
System.out.println("初始化块");
}
//子类构造方法
Test() {
System.out.println("Son"+this.i);
}
public void test(){
System.out.println("son test"+this.i);
}
public static void main(String[] args) {
System.out.println("在我之前是啥");
Test tt=new Test();
}
}
//输出为
父类--静态变量
父类--静态初始化块
子类静态变量
子类静态初始化块
在我之前是啥
-----------
父类--变量
父类--初始化块
Father5
son test0 //此步为子类实例化父类,后面说明
子类变量
子类初始化块
-
子类调用new 时,在调用子类构造函数之前,会先调用父类的无参构造函数,并且每一个构造函数都会调用。
即隐式调用super()。若父类一个构造函数也没有,也会隐式调用。若父类只有含参的构造函数,没有无参的构造
函数。则子类必须显示的调用父类的含参构造函数,即super(参数),且super必须位于第一句。
子类通过this()调用子类本身的其他构造函数,但是不会和super()同时出现,因为调用构造函数必须是第一句。
在静态方法中,不能使用this,this指向对象,static修饰的变量或方法优先于对象而存在。 -
子类实例化父类对象 此对象调用的方法必须是父类拥有的,如果被子类重写,则调用的是子类的
Father fa=new Son();
当子类实例化父类对象时,该对象只能调用父类中定义的方法和属性。
因为属性不能重写,所以该对象访问的对象是父类的
因为静态方法也不能重写,所以访问的也是父类的,因为静态方法是类在加载时就被加载到内存中的方法,在整个运行过程中保持不变,因而不能重写。
非静态方法是在对象实例化时才单独申请内存空间,为每一个实例分配独立的运行内存,因而可以重写
因为非静态方法可以被重写(包括抽象方法),所以当调用方法时,原则是调用父类的方法,但是被子类重写了,就会调用子类的方法。产生多态或者动态绑定。 如果此方法没有被重写,则只会调用父类方法,不会调用子类独有方法。
重写原则 两同两小一大
1. 方法名相同,参数类型相同
2. 子类返回类型小于等于父类方法返回类型
3. 子类抛出异常小于等于父类抛出异常
4. 子类访问权限大于等于父类访问权限
class Father(){
int i=5;
Father(){
syso("father "+i);
test();
}
pulic void test(){
syso("father test"+this.i).
}
}
class Son(){
int i=50;
Son(){
syso("son "+this.i);
}
public void test(){
syso("Son test"+this.i);
}
pulic static void main(String[] args){
Father fa=new Son();
fa.i;
}
}
// father 5
son test 0
son 50
5
//先调用父类构造方法,父类构造方法中,调用了test(),但是子类重写了此方法,所以调用的为子类的方法(多态),但是此时还没调用子类的构造方法,所以子类中i的值为0。
fa.i 调用父类的属性。
- 接口
A) 接口没有提供构造方法(初始化块),抽象类中可以有构造方法(初始化块),接口是特殊的抽象类,接口和抽象类都不能实例化
B) 接口中的方法默认使用public、abstract修饰,因此不要企图使用proteced或是private修饰符去修饰方法,同时由于接口中的方法都是abstract的所以方法不能有具体实现,即方法后不能有{},抽象类中可以有实现过的方法。接口不能定义静态方法,抽象类可以定义静态方法。
C) 接口中的属性默认使用public、static、final修饰,因此不要企图使用proteced或是private修饰符去修饰属性,同时由于接口中的属性是public static final的那么定义属性时必须初始化,否则会报错。抽象类即可以定义普通成员变量也可以定义静态常量
D) 接口可以多继承
接口与实现类之间也存在多态关系,即可以用接口声明变量,然后实现类指向接口变量
- 多态
多态三个条件
1. 继承
2. 重写
3. 父类引用指向子类对象
- 重载
重载是指同一类中,方法名相同,参数个数或者类型不同,至于返回值类型,修饰符等没有关系。如果已经构成重载,则方法的返回值可以不同,如果两个方法的参数列表完全一样,不能设置返回值类型不同来实现重载,会提示已定义此方法。
- Interger 类将-128~127之间的整数放入一个缓存数组中,所以在此范围的装箱,实际上引用的数组的元素。
不在此范围的就会新建Integer实例
Interger ina=2;
Interger inb=2;
syso(ina == inb) //true
Interger ina=128;
Interger inb=128;
syso(ina == inb) //false
单例类
class Singleton{
private static Singleton instance; //使用当前类变量来缓存是否已创建实例,已创建则直接返回
private Singleton(){} //构造器使用private修饰,隐藏该构造器
//提供一个静态方法,用于返回实例,并且保证只返回一个对象
public static Singleton getInstance(){
//如果instance为null,说明还没创建过该对象
//如果不为null,表明已经创建该对象,直接返回该对象
if(instance == null){
instance =new Singleton();
}
return instance;
}
}
public class SingletTest{
public static void main(){
//创建对象不能通过构造器,只能通过类静态方法创建
Singleton s1=Singleton.getInstance();
Singleton s2=Singleton.getInstance();
syso(s2 == s2); //会输出true,因为只会产生一个对象
}
}
//上面是懒汉式,多线程调用会产生多个实例。下面是饿汉式
class Singleton{
//static final修饰,类加载时就初始化
private static final Singleton instance=new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
//静态内部类 懒加载 因为静态内部类不会在类加载时加载
public class Singleton{
private Singleton(){}
private static class SingletonHolder{
private static Singleton instance = new Singleton();
}
public static getInstance(){
return SingletonHolder.instance;
}
}
final final修饰的变量必须显式初始化
static 和 final
-
satic 修饰的字段在类加载的过程中的准备阶段被初始化为0或null或默认值,而后在初始化阶段(调用<clinit>才会被设定值,如果没有设定值,就是默认值)
-
final修饰的字段在运行时被初始化
-
static final 修饰的字段没有默认值,必须显示的赋值。即编译期间放入常量池
- final修饰局部变量 可以在定义是指定默认值,也可以随后赋值,但只能赋值一次,修饰形参时,不能在方法中修改
public void test(final int a){
a=5; //错误,不能修改final形参
}
- final修饰成员变量
- 类变量 必须定义时赋值,或者在静态初始化块中赋值
- 实例变量 必须在定义时赋值,或者在非静态初始化块,或者构造函数中赋值
- final 修饰基本类型和引用类型的区别
- 修饰基本类型,因为基本类型不能重新赋值,所以不能被改变
- 修饰引用类型,对于引用变量保存的只是一个引用,final只能保证这个变量引用地址不会改变,即不会引用别的对象了。但是对象的值是可以改变的
final int[] arr={1,2,3,4};
Arrays.sort(arr); //可以进行排序
arr[3]=5; //可以进行赋值
arr=null; //重新赋值,非法
fianl Person p=new Person();
p.setName(); //可以重新设置属性值
final String str1="abc" + 123;
final String str2="abc"+String.valueOf(123);
str2 调用了String类的方法,所以无法在编译时确定值,所以str1不等于str2
- 如果final修饰的变量,访问了普通变量,调用了方法,则该变量就无法在编译时确定
final String str1="abc" + 123;
final String str2="abc"+String.valueOf(123);
str2 调用了String类的方法,所以无法在编译时确定值,所以str1不等于str2
String s1="abcd";
String s2="ab" + "cd";
s1 == s2 //true
String s4="ab";
String s5="cd";
String s3=s4 + s5;
s3==s1 //false s3无法在编译时确定值,s4和s5只是普通变量,不回进行常亮替换,s4和s5加上final修饰符,s3即等于s1
- fianl 修饰方法
final修饰的方法子类无法重写,但是子类可以重新定义不加final修饰的同名方法。final方法无法重写,但是可以被重载。类的private方法被隐式定义为final方法
缓存类
class CacheImmutale{
private static int MAX_SIZE=10;
//使用数组缓存已有的实例
private static CacheImmutale[] cache =new CacheImmutale[MAX_SIZE];
private static int pos=0; //记录缓存实例在缓存中的位置,cache[pos-1]是最新缓存的实例
private final String name;
private CacheImmutale(Strin name){//构造方法私有
this.name=name;
}
public String getName(){
return name;
}
public static CacheImmutale valueOf(String name){
//遍历已缓存的对象
for(int i=0;i<MAX_SIZE;++i){
//如果已有实例,则直接返回
if(cache[i]!=null && cache[i].getName().equals(name)){
return cache[i];
}
}
if(pos==MAX_SIZE){ //如果缓存池已满
cache[0]=new CacheImmutale(name); //覆盖第一个对象
pos=1;
}else{
cache[pos++]=new CacheImmutale(name); //新创建的对象缓存起来
}
}
public boolean equals(Object obj){
if(this==obj){
return true;
}
if(obj!=null && obj.getClass() == CacheImmutale.class){
CacheImmutale ci=(CacheImmutale)obj;
return this.name.equals(ci.getName());
}
return false;
}
public int hashcode(){
return name.hashcode();
}
}
CacheImmutale c1=CacheImmutale.valueOf("hello");
CacheImmutale c2=CacheImmutale.valueOf("hello");
syso(c1 == c2); //true
Interger类就采用额上述相同的策略,如果采用new 创建Integer对象每次都返回新的对象,而采用valueOf()方法,则会缓存该对象
Interger i1=new Interger(6);
Interger i2=Interger.valueOf(6);
Interger i3=Interger.valueOf(6);
syso(i1 == i2) //false
syso(i2 == i3) //true
Interger i2=Interger.valueOf(128);
Interger i3=Interger.valueOf(128);
syso(i2 == i3) //false 由于Integer值缓存-128~127之间的值,所以超过此范围不缓存
- final类不能被继承
抽象类
- abstract 和 fianl不能同时使用
- abstract 和static不能同时修饰方法,因为通过类调用没有方法体的方法,会出错,但是可以同时修饰内部类
- abstract方法必须被重写,所以private和abstract不能同时修饰方法
内部类
-
外部类只有两种修饰符,默认修饰和public,而内部类可以有4种访问权限,private,protected,public,default。外部类也不可以使用static,内部类可以
-
非静态内部类,内部类中使用用同名属性的话,通过 外部类名.this.name 访问外部类属性,this.name 访问自己的属性
-
非静态内部类可以访问外部内的private成员和static成员,因为有一个指向外部类的引用但是外部类不能访问内部类成员,必须显示创建内部类对象来访问属性
-
在外部静态成员中不允许使用非静态内部类。非静态内部类也不能有静态方法或静态成员变量。除非是
final static 常量,并且常量值要编译期间就确定。 -
非静态内部类对象必须依赖于外部类对象。必须先创建外部内对象,然后才能创建内部类对象。
-
静态内部类 类加载时内部类(静态和非静态)都不会被加载,静态内部类调用其静态方法时才被加载
1. 静态内部类可以定义静态成员和非静态成员变量
2. 只能访问外部静态成员。外部类也不能直接访问静态内部类成员,必须使用静态内部类
类名或者内部类对象访问
3. 静态内部类 的外部类就像是一个包一样,在不是外部内的其他类中可以直接使用静态内部类定义对象
只要导入该静态内部类。或者使用 outClass.StaticInnerClass tt = new outClass.StaticInnerClass()
public class OutClass{
//非静态内部类
public class InnerClass{}
//静态内部类
public static StaInnerClass{}
}
class Test{
main(){
OutClass.InnerClass obj1 = new OutClass().new InnerClass(); //非静态内部类对象
OutClass.StaInnerClass obj1 = new OutClass.StaInnerClass(); //静态内部类
}
}
匿名内部类
-
匿名内部类必须继承一个父类或者实现一个接口,但最多也是一个
-
匿名内部类不能是抽象类,因为系统在创建匿名内部类时,就会立即创建匿名内部类对象。
当通过实现接口创建内部类,不能定义构造器,因为没有类名,所以new括号里,没有参数。而通过继承父类创建内部类,可以传参,调用的是父类的构造方法。
但是可以有初始化块。 -
匿名内部类中不能存在任何的静态成员变量和静态方法。
-
常用的方式就是创建接口类型的对象。new Runnable(){public void run(){})。
-
被匿名内部类访问的局部变量必须用final修饰,java8默认final修饰,所以可以不加
public class Test {
public static void main(String[] args) {
}
public void test(final int b) {
final int a = 10;
new Thread(){
public void run() {
System.out.println(a);
System.out.println(b);
};
}.start();
}
}
a的值在编译期间就可以确定,则直接在匿名内部类中创建一个拷贝;而b的值无法在编译期间确定,则通过构造函数传参的方式进行初始化。为了防止数据不一致性,所以加了final.
因为方法的生命周期和方法内部类对象的生命周期不一致。方法中的局部变量,方法结束后就释放掉,内部类就找不到了。加上final的话,内部类就会拷贝一份到内部类中,值就不会改变。
创建静态内部类对象的一般形式为: 外部类类名.内部类类名 xxx = new 外部类类名.内部类类名()
创建成员内部类对象的一般形式为: 外部类类名.内部类类名 xxx = 外部类对象名.new 内部类类名()
JVM
内存区域 包括程序计数器,Java虚拟机栈,本地方法栈,Java堆,方法区
- 程序计数器
每条线程都有一个程序计数器,各线程之间的计数器互不影响,因此该区域是私有的,并且不会有内存溢出异常。
- 虚拟机栈
-
该区域也是私有的,它的声明周期也与线程相同。描述的是Java方法执行的内存模型,每个方法执行的时候都会同时创建一个栈帧。栈顶栈帧关联的是当前方法。栈帧用于存储局部变量表,操作数栈,动态链接,方法返回地址等信息。并且在编译期间,栈帧分配的内存一确实,运行期间不会改变
-
栈是用于支撑虚拟机进行方法调用和方法执行的数据结构,调用方法的过程就是压栈和出栈的过程 。
-
该区域会抛出两中异常:如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常;如果无法申请到足够的内存空间,则抛出OutOfMemoryError
2.1 局部变量表
存放方法参数和方法内部定义的局部变量。局部变量表所需的内存空间在编译期间完成分配,运行期间不会改变。
- Java堆 Java堆和方法区是线程共享的
- 对象和数组都分配在堆内存。是GC主要管理的区域。堆内寸可以物理上不联系,逻辑上连续。会抛出抛出OutOfMemoryError异常。
- 方法区
-
方法区是各个线程共享的区域,也被称为"永久代"。运行时常量池,用于存放编译器生成的字面量和符号引用。运行时常量区具有动态性,比如String的intern()方法。
-
常量池主要有字面量和符号引用;字面量包括字符串和final常量。符号引用包括:带有包名的class名,字段的名称和(private,static等修饰符),方法的名称和(private,static)等修饰符
对象实例化
- Object obj = new Object() 会涉及到Java栈,Java堆,方法区三个区域。
每个class文件的前四个字节成为魔数,作用是确定这个文件能否被虚拟机接受
接下来的是文件版本号,5,6字节是次版本号,7,8字节是主版本号
类加载机制
- 类加载包括 加载 -> 链接(验证,准备,解析) ->初始化 ->使用 -> 卸载。
其中加载、验证、准备和初始化四个阶段是顺序开始,不一定顺序完成。解析阶段顺序不确定,因为存在动态绑定,可能在初始化之前,也可能在初始化之后
绑定 指的是一个方法的调用与方法所在的类关联起来
静态绑定 编译器间绑定 final,static,private和构造方法是静态绑定
动态绑定 运行时绑定,大部分方法都是后期绑定
- 加载
将class文件加载到内存,在堆中生成一个java.lang.Class对象,作为方法区数据的访问入口。
- 类加载器
类加载器从上到下分为 启动类加载器,扩展类加载器,应用程序类加载器和自定义类加载器。
即使两个类源于同一个class文件,只要类加载器不同,这两个类也不相等。
1. 启动类加载器 负责加载JDK\jre\lib,c++实现,其他几个类加载器都是Java实现
2. 扩展类加载器 负责加载JDK\jre\ext目录,
3. 应用程序类加载器 负责加载ClassPath,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
这些类加载器上面的都是下面的父类,但是并不是通过继承实现的,而是通过双亲委派模型实现的,即如果一个类收到了类加载请求,首先不会自己加载这个类,而是委托给父类加载,依次向上。只有当父类加载器无法完成加载时,子类才会去自己加载。这样可以保证加载的安全,比如你自己定义了一个String,Object类,是无法使用过,因为上层加载器会先加载系统的类,
自定义的类就不会被加载。
- 准备
-
准备阶段是为仅包括static变量,分配在方法去中,而不包括实例变量实例变量在对象实例化时分配内存。
-
此时static变量的值是系统默认的值(0,null,false)等,而不是显示赋予的值
public static int value = 3 ;
value在准备阶段的值为0,而不是3,赋值为3是调用类构造器<clinit>()所以赋值为3是在初始化阶段执行的。
public static final int value = 3; //编译期间值已经放入常量池中
而static final 修饰的变量,必须显示的赋值,在编译期间值已经确定,所以在准备阶段值也为3
- 解析 将常量池中的符号引用转换为直接引用得过程 包括四种解析
- 类或接口解析
- 字段解析
字段解析时会现在本类中寻找,会现在本类中寻找,若未找到,然后再在接口和父接口中寻找,然后再到父类,祖类中寻找。此处有子类直接引用父类定义的静态字段,只会触发父类的静态初始化块,不会触发子类的初始化块。
Super{
static int m=100;
static{
}
}
Father extends Super{
static int m=10;
static{
}
}
Son extends Father{
static int m=1;
static{
}
}
Class{
main(){
Son.m
}
}
//Son不注释m,则三个静态块都会打印,注释Son的,会打印Super和Father的
//注释Father的,只会打印Super的
并且实现的接口和父类中若定义相同的变量,编译器会出错
- 类方法解析
- 接口方法解析
- 初始化
-
初始化阶段是执行类构造器 <clinit>()的过程
-
初始化时遇到 new 时,会先加载静态块和静态字段
1. 静态字段只有直接定义这个字段的类才会被初始化,因此,如果父类直接引用父类特有,而自己没有的静态字段,只会触发父类的初始化而不会触发子类的初始化。
2. static final 字段在编译已经存入常量池中,所以也不会调用静态初始化块。
但是父类的还会调用。
3. 类数组定义,也不会触发初始化块。
main (){
ClassName[] arr = new ClassName[10]; //此时数组中的元素只有一个引用,还未初始化
for(ClassName a: arr
)
{
a = new ClassName(); //加上此句,就会触发初始化
}
}
1. <clinit>() 执行时,静态块和静态字段是按照顺序执行的,静态语句块只能访问到它之前定义的静态变量,其之后定义的变量,只能赋值,不能访问,输出其值。
2. <clinit>() 会默认先执行父类的 <clinit>() 方法
3. 如果没有静态块和静态变量 <clinit>()可以不执行
4. 执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
垃圾回收
- 对象的三种状态
-
可达状态 对象被创建后,有一个以上的引用指向它
-
可恢复状态 程序中某个对象,不再有任何引用变量指向它,则进入可恢复状态。在这种状态下,垃圾回收机制准备回收该对象,
在回收之前,会先调用finalize()方法进行资源清理,如果调用finalize()重新让一个引用变量引用该对象,则该对象变为可达状态,
否则该对象进入不可达状态。 -
不可达状态 一个对象彻底没有引用变量指向,且已经调用finalize()方法,即为不可达状态,然后系统回收。
- 强制垃圾回收
- 当一个对象失去引用,系统何时回收它,对程序透明。程序只能控制对象不再被引用,不能控制何时被回收。但是可以强制系统进行回收。
(但是也是通知系统进行回收,何时回收依然不确定)
强制系统回收有两种方式
1. System.gc()
2. Runtime.getRuntime.gc()
在垃圾回收之前,会调用finalize()进行清理资源,finalize()有如下特点
1. 永远不要主动调用某个对象的finalize()方法,该方法应该交给垃圾回收机制调用
2. finalize()何时被调用,是否被调用,具有不确定性,不一定会执行
3. 当JVM执行finalize(),可能使该对象或者系统中的其他对象变为可达状态
4. 当JVM执行finalize()出现异常时,垃圾回收机制不会报告异常,程序继续执行。
垃圾对象的判定
- 引用计数法
给对象添加引用计数器,有引用指向它,计数器加1,引用失效,计数器减1,当引用为0时,即被回收。但是无法处理对象相互循环引用的问题。
public Class{
private Object obj;
public void test(){
Test t1 = new Test();
Test t2 = new Test();
//循环引用
t1.obj = t2;
t2.obj = t1;
t1 = null;
t2 = null ;
//将t1,t2置为null,即t1,t2指向的对象不能再访问,但是它们的引用计数器不为0,无法GC
}
}
- 根搜素算法
将可以作为GC ROOT的对象作为起始点,从此点开始搜索,搜索和该节点有直接引用和间接引用的关系的对象,将这些对象以链的形式组合起来,形成一个图。然后不在这张关系网中的节点,就会被回收。即使这些回收节点之间有关联。
将可以作为GC ROOT的对象有:
1. 虚拟机栈中引用的对象
2. 本地方法栈中引用的对象
3. 方法区中静态属性引用的对象
4. 方法区中常量引用的对象
在根搜索算法中,回收一个对象,会进行两次标记。第一次标记是不在关系网中的对象,第二次的就是判断该对象是否重写了finalize()方法,如果没有实现就直接回收,如果实现了就会先将该对象放入一个队列中,然后线程会创建一个低优先级的线程去执行finalize()方法,但是finalize()方法只能执行一次,如果在finalize()方法中,该对象重新加入关系网,就复活,否则就回收。
垃圾收集算法
- 标记--清除算法
此算法就是采用根搜索算法进行标记,标记完成后,再扫描整个空间中未被标记的对象,进行回收。
优点:不需要进行对象移动,只对不活动的对象进行处理
缺点:直接回收不存活的对象,所以会造成空间碎片,当再次需要分配一个较大的对象而需要连续的空间时,就会再次触发垃圾收集。
- 复制算法 适合于新生代
复制算法是标记-清除算法的改进。它将内存分为大小相等的两块,每次只使用其中的一块,当一块的用完后,就将还存活的对象复制到另一块内存上面,然后把使用过的内存一次性清理掉。
优点:每次只对一块内存进行回收,运行高效。不会存在碎片。
缺点: 每次只能分配一半的内存。
- 标记--整理算法 适合于老年代
此算法的标记过程同标记-清除的算法一样,只是对标记的垃圾对象处理不同。它不是直接对垃圾对象进行清理,而是将存活的对象向一端移动,然后清楚边界外的内存。
优点:解决了内存碎片的问题
缺点:多了对象的移动,成本加大。
- 分代收集算法 不同对象的生命周期不一样,所以采用分代收集算法
Java堆可以分为新生代,老年代,永久代
- 新生代
所有新生成的对象都放在新生代,新生代的生命周期比较短,新生代采用复制算法。
新生代内存按照8:1:1的比例分为一个Eden区和两个Survivior区。对象会在Eden区创建。
回收时先将Eden区的存活对象复制到一个Survivir0区,然后清空Eden区,当Survivir0区也存满了,则将Eden区和Survivir0区存活对象复制到另一个Survivir1区,然后清空Eden区和Survivir0区。
此时Survivir0区是空的,就将Survivir0区 和Survivir1区交换,即保持Survivir1区为空,如此反复。当Survivir1区不足以存放Eden区和Survivir0区的存活对象时,就将存活对象直接放到老年代。
新生代的对象在进行一次复制回收(Minor GC)的时候,年龄就会增加一岁,当岁数到一定年龄(默认15),就会进入老年区。
- 老年代
老年代都是生命周期比较长的对象。
老年代采用标记-整理方法收集,老年代默认是占用了68%就会触发Full GC
- 持久代
用于存放静态文件,如Java类,方法等,对GC没有显著影响
四种引用方式
- 强引用(StrongReference)
new创建的对象就是强引用,就算内存不足,也不会回收该对象类解决内存不足。
- 软引用(SoftReference)
软引用通过SoftReference类实现,对于只有软引用的对象,当系统内存空间足够时,不会被系统回收,程序也可以使用该对象。当内存不足时,系统可能回收它。
软引用通常位于内存敏感程序中,适用于缓存
- 弱引用(WeakReference)
弱引用通过WeakReference类实现。对于只有弱引用的对象而言,不管内存是否足够,总会回收该对象。
- 虚引用(PhantomReference)
虚引用通过PhantomReference类实现, 虚引用和没用引用的效果大致相同。虚引用不能单独使用,必须和引用队列(ReferenceQueue)使用
虚引用无法获取它所引用的对象,所以get输出null
上面三个引用都有一个get()方法,获取被引用的对象
泛型
- 泛型类
public class Test<T>{
Test(){}
Test(T info){ //构造函数还是原来的
}
}
Test<String> t1=new Test<String>();
- 泛型类只有一个类
List<String> 和 List<Integer> 实际上是相同的类型,即List
syso(li.getClass() == l2.getClass()) //返回true
- static 无法访问泛型参数,因为类还没实例化
静态方法,静态初始化块,静态变量的声明不能使用泛型
static T info ; //错误
public static void test(T aa){} //错误
类型通配符
- 如果Foo是Bar的子类型,G是一种带泛型的类型,则G<Foo>不是G<Bar>的子类型,所以当一个方法的形参是
各种类型,就需要通配符。 比如List<String> 不是List<Object>的子类
List<?> 表示是各种泛型List的父类。
- 但是不能把元素加入到其中,因为无法确定集合中的类型,除了null。但是可以用get()方法取元素,因为存储的一定是Object。
List<?> c=new ArrayList<String>();
c.add(new Object()); //compile time error,不管加入什么对象都出错,除了null外。
c.add(null); //OK
- 类型通配符的上限 只代表某一类型泛型的父类
- public void drawAll(List<? extends Shape> shapes)只接收元素类型为Shape子类型的列表作为参数了。
-
类型通配符的下限 <? super Type> 代表必须是Type本身,或是Type的父类
-
如果你想从一个数据类型里获取数据,使用 ? extends 通配符
如果你想把对象写入一个数据结构里,使用 ? super 通配符
如果你既想存,又想取,那就别用通配符
线程 进程是资源分配的基本单位,线程是CPU调度的基本单位
- main()方法是默认的主线程
- Thread.currentThread() //返回当前正在执行的线程对象
- getName() //返回调用该方法的线程名称,默认情况下,主线程的名称为main,用户启动多个线程名称为
Thread-0,Thread-1....
- 线程创建
- 继承Thread类创建线程类,然后直接调用start方法
- 无法共享线程类的实例变量(属性)
class Runner2 extends Thread{ //继承Thread类创建线程
public void run(){
for(int i=0;i<50;++i){
System.out.println("Runner : "+i);
}
}
}
Runner2 r=new Runner2();
r.start();
- 实现Runnable接口创建线程类,依次实例作为Thread的target来创建Thread对象,该Thread才是真正的线程对象
- 可以共享线程类的实例变量(属性)
class Runner1 implements Runnable{ //实现Runnable接口创建线程
private int i=0; //此时多个线程共享一个实例变量
public void run(){ //线程执行体
//int i=0; //若是局部变量的话,每个线程都有一个局部变量的拷贝,所以互不影响
for(;i<10000;++i){
System.out.println(Thread.currentThread().getName()+" " +i);
}
}
}
Runner1 r=new Runner1();
Thread th=new Thread(r);
th.start();
Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅仅作为线程执行体,
而实际的线程对象依然是Thread实例,多个线程共享一个target对象
线程状态
- 在线程的生命周期中,有新建,就绪,运行,阻塞和死亡状态
- 使用new创建了一个线程之后,该线程就处于新建状态
- 当线程对象调用了start()方法,该线程就处于就绪状态,会为其创建方法调用栈和程序计数器
- 执行run()方法进入运行状态
- 调动sleep()等等,线程会进入阻塞状态
- 线程从阻塞状态只能进入就绪状态,无法直接进入运行状态
- 阻塞状态又可以分为三种
- 等待阻塞 运行状态中的线程执行wait()方法,使本线程进入等待阻塞状态
- 同步阻塞 线程在获取synchronized同步锁失败(锁被其他线程占用),会进入同步阻塞状态
- 其他阻塞 调用sleep()或join()或者发出了I/O请求,进入阻塞状态,当结束后重新转入就绪状态
-
isAlive()方法 当线程处于就绪,运行和阻塞状态时,将返回true,当处于新建和死亡时,返回false
-
不要对死亡的线程再次调用start()方法,或者新建状态再次调用start()方法,会引发异常
-
后台进程
- 所有的前台进程都死亡,后台进程自动死亡
- setDaemon(true),设置为后台进程,必须在start()方法之前调用,isDaemon()判断是否为后台进程
- 线程结束
线程结束的三个原因
1. run()执行完毕,线程正常结束
2. 线程抛出一个未捕获的异常或者Errror
3.直接调用线程的Stop()方法(不建议使用,容易死锁)
控制线程
- join() 当在某个程序中调用join(),调用线程将被阻塞 直到被join()方法加入的join线程执行完毕
- 通常用于将大问题划分许多小问题,每个小问题分配一个线程,当所有小问题处理完毕后,再调用主线程操作
-
sleep() 暂停线程执行,进入阻塞状态
-
yield() 线程让步,让当前线程暂停一下,但是不会阻塞该进程,只是进入就绪状态。让系统的线程调度器重新调度一次
所以当某个线程调用了yield(),有可能线程调度器又将其调度出来使用。当某个线程调用了yield方法,只有
优先级与当前线程相同或者高的线程才会获得执行机会。
sleep()和yield()的区别
- sleep()暂停当前线程后,不会区分其他线程的优先级,而yield只会给优先级相同,或者更高的线程执行
- sleep会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态,而yield不会将线程转入阻塞状态
只是强制当前线程进入就绪状态,所以有可能某个线程调用yield方法暂停之后,立即再次获得资源而执行 - sleep方法声明抛出了InterruptedException异常,所以需要捕获该异常或者抛出异常,yield没有声明
抛出异常 - sleep比yield更好
- 线程优先级
- 每个线程默认的优先级都与创建它的父进程的优先级相同
- setPriority() getPriority() 设置和获取优先级
- 垃圾回收器的线程优先级比较低
Thread类的优先级的静态常量
MAX_PRIORITY //值为10
MIN_PRIORITY //值为1
NORM_PRIORITY //值为5 默认优先级
死锁
- 死锁的四个必要条件
1. 互斥条件 一个资源每次只能被一个进程使用
2. 请求与保持 一个进程隐请求资源而阻塞时,对已经获得的资源保持不放
3. 不剥夺 进程已获得资源,在未使用之前,不能强行剥夺
4. 循环等待 若干进程之间形成一种头尾相接的循环等待资源关系
只要有一个不满足,就不会死锁
线程同步
- synchronized
-
每一个对象有且仅有一个同步锁,所以同步锁是依赖于对象存在。调用某个对象的synchronized方法时,就获取了该对象的同步锁
-
不同线程调用同一对象的同步锁是互斥的
-
synchronized 方法
public synchronized void foo(){
xxxxx
}
- synchronized 代码块
public void foo(){
synchronized(this){
xxxxxx
}
}
synchronized代码块this指代调用此方法的当前对象,可以换成其他对象,就成为其他对象的同步锁
- synchronized 基本规则
-
当一个线程访问某个对象的synchronized方法或者代码块,其他线程对该对象的 该同步方法或者代码块 访问将被阻塞
-
当一个线程访问某个对象的synchronized方法或者代码块,其他线程仍然可以访问该对象的 非同步方法(可能影响同步方法中访问的变量)
-
当一个线程访问某个对象的synchronized方法或者代码块, 其他线程访问该对象的 其他的同步方法也会被阻塞
-
当一个线程访问一个类的静态synchronized方法,其他线程就不能调用该类的其他静态synchronized方法了,但是可以调用非静态的synchronized方法
- 规则1
class MyRunable implements Runnable{
public void run(){
synchronized(this){
try{
for(xxx){
Thread.sleep(100);
syso(xxx);
}
}catch(InterruptedException e){
}
}
}
}
Runnable rn=new MyRunable();
Thread t1=new Thread(rn,"t1");
Thread t2=new Thread(rn,"t2");
t1.start();
t2.start(); //此时t1,t2 访问的都是rn对象的同步方法,所以会阻塞打印
-------------------------------
class MyThread extend Thread{
MyThread(name){
super(name);
}
public void run(){
synchronized(this){
xxxxx
}
}
}
Thread t1=new MyThread(t1);
Thread t2=new MyThread(t2);
t1.start();
t2.start(); //因为t1,t2是不同的对象,调用的方法是不同对象的同步方法,所以不会阻塞,会轮流打印
-
规则2 一个方法加锁,一个没加,两个线程访问同一对象的该两个方法,也不会阻塞
-
规则3 两个线程访问同一对象的不同的 同步方法,也会阻塞
- 实例锁和全局锁
- 实例锁 锁在某个实例对象上,对应synchronized
- 全局锁 锁在某个类上,即obj.getClass,无论有多少个对象,线程都会阻塞访问。对象 static synchronized
public class Test{
synchronized void MethodA();
synchronized void MethodB();
static synchronized classMethodA();
static synchronized classMethodB();
}
- t1.MethodA() 和 t1.MethodB() 不能同时访问,因为是同一个对象的同步锁
- t1.MethodA() 和 t2.MethodA() 可以同时访问,因为不是同一个对象的同步锁
- t1.classMethodA() 和 t2.classMethodB() 不能同时访问,因为这两个方法都是静态锁,锁在类上,相当于Test.classMethodA()和Test.classMethodB()
- t1.MethodA() 和 Test.classMethodA() 可以同时访问 MethodA()是对象的锁,classMethodA()是类的锁
Lock
- Lock是一个接口,常用的是ReentranLock(重入锁)
Lock lock = new ReentranLock();
与synchronzied区别
线程通信 三个方法都属于object,因为这三个方法都依赖于同步锁,而同步锁是是对象持有
-
wait() 线程进入等待阻塞状态,同时释放同步锁,后面的方法不会执行,需要捕获异常
-
notify() 唤醒同步锁上的一个线程对象,但是此时并没有完全释放锁,后面的代码继续执行,直到当前线程结束,不需要捕获异常
-
notifyAll()
-
对于synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以直接调用以上
-
对于synchronized修饰的同步代码块,同步监视器是括号里的对象,所以要使用该对象调用
-
wait 和 sleep的区别
主要区别wait后会释放同步锁,而sleep不会释放锁 -
lock使用Condition进行通信 Condition fullCondition = lock.newCondition();Condition对应的方法为await(),signal(),signalAll()
volatile
- 并发编程三个概念
- 原子性
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
只有语句1是原子操作,其他都不是
语句2 包括两个操作,首先读取x的值,然后讲x的值写入工作内存
语句3和4 包括两个操作 读取x的值,然后进行加1操作,写入新的值
java中对long、double类型的读分为两部分,先读前32位,然后读剩余32位
Voliate可以保证对long的读写是原子性的
- 可见性
当一个共享变量被volatile修饰后,它会保证修改的值立刻被更新到主存中,其他线程再需要读取时,就会重新去内存中读取 - 有序性
- 主内存和工作内存
-
类的成员变量,静态变量和数组元素,存储在主内存(物理内存),是线程之间共享的
-
局部变量,方法定义参数和异常处理参数存储在工作内存中,线程之间不共享
-
当线程访问某个对象的时候,把主内存中的值加载到本地内存中,建立一个变量副本,然后修改的话,只修改本地的值,在修改完某一时刻,再更新到主内存中
- volatile
-
volatile写:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
-
volatile读:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
-
volatile可以保证可见性,但是不能保证原子性
线程安全集合 并发类
- List
-
CopyOnWriteArrayList
相当于线程安全的ArrayList
,实现了List接口,通过volatile和互斥锁达到线程安全
实现了List接口,因此是个队列;包含了成员变量lock,实现对CopyOnWriteArrayList的互斥访问
CopyOnWriteArrayList 采用volatile数组来保持数据。执行读操作时,直接操作集合本身,无需加锁与阻塞。当执行写入操作,包括(add,remove,set)等方法,因为底层采用的是,复制一份新的数组,然后对数组操作,所以写操作比较麻烦,适合读操作大于写操作,比如缓存。修改时通过volatile数组使读到的
数据总是最新的;修改时会先获取互斥锁,修改完毕后,再将数据更新到volatile数组,然后再释放锁,实现线程安全
CopyOnWrite容器是一种读写分离的容器,读的时候不需要加锁,如果有多个线程同时向ArrayList添加数据,读的还是读的旧的数据,因为写的时候不会锁住旧的
ArrayList。只能保证数据的最终一致性,不能保证数据的实时一致性。
1.getArray()和 setArray(Object[] a)分别用来获取和设置volatile数组的值。CopyOnWriteArrayList的构造函数都会调用setArray()。
2. add(E e)
在添加操作开始前,获取互斥锁,此时若有其他线程要获取锁,则必须等待。然后新建一个数组,将原数组拷贝到新数组中,新数组length=原数组length+1,多出来
的为即存放添加的元素。然后调用setArray()将新数组的元素赋值给原volatile数组。然后释放互斥锁。
在执行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新的对象
(在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)
3. get(index) 获取volatile数组的index元素
4.先获取互斥锁,然后复制原数组,如果删除的是最后一个元素,则直接拷贝最后一个元素之前的元素 然后赋值到原数组;若不是最后一个元素,则将index之前和之
后的元素拷贝到新数组,然后赋值给volatile数组
5. 遍历
CopyOnWriteArrayList的iterator返回COWIterator对象,COWIterator实现了ListIterator接口,但是不支持修改元素的操作,对于remove(),set(),add()等操作,COWIterator都会抛出异常!并且迭代时不会抛出ConcurrentModificationException异常。
- Set
-
CopyOnWriteArraySet
相当于线程安全的HashSet
,继承于AbstractSet类,底层封装了CopyOnWriteArrayList,通过它实现的,同样不允许重复。
HahSet虽然也继承于AbstractSet,但是HashSet是通过HashMap实现的。 -
ConcurrentSkipListSet
相当于线程安全的TreeSet
,继承与AbstractSet类,并且实现了NavigateSet接口,是通过ConcurrentSkipListMap实现的
- Map
-
ConcurrentHashMap
相当于线程安全的HashMap
,继承与AbstractMap类,并且实现了ConcurrentMap接口,通过锁分段实现并发,key和value不为null
ConcurrentHashMap数据结构是由Segment数组和HashEntry数组,而HashEntry其实是一个链表。Segment是ConcurrentHashMap的内部类,此内部类里包含了一个HashEntry数组,HashEntry也是ConcurrentHashMap的内部类,存储的是键值对,类似HashMap的Entry。所以Segment类似于HashMap
ConcurrentHashMap类似于由Segment数组构成。
Segment继承与ReentrantLock,所以可以实现锁机制。包含了一个HashEntry数组table;count变量是table数组包含的HashEntry对象的个数。
HashEntry的key,hash,next域被申明为final,value被声明为volatile,所以HashEntry插入时只能头插入
- 初始化
默认情况下,Segment的数组长度为16,Segment中默认为2,且必须为2^n
- get(key)
get过程不需要加锁,因为get使用到的变量都是volatile,比如用于统计当前Segment大小的count,以及HashEntry的value。因为volatie字段的写入操作
先于读操作,所以get读到的是最新的值。找到则返回value,否则返回null。 ConcurrentHashMap不允许key和value为null,因为get时读到value为null值
则可能是另一个线程修改了,此时需要加锁,重新获取。
- put(key,value) 只锁定某个segment,不是整个ConcurrentHashMap
value为null时,会抛出异常。加锁,定位到Segment,定位到HashEntry。如果key-value已存在则覆盖旧值,返回旧值;若不存在,则新建节点,添加到链表的头部
返回null。然后解锁。如果超过容量时,扩容是对当前Segment扩容。
- remove(key)
先加锁,将key之后的元素连接到新链表,然后讲key之前的元素克隆到新链表中,按照头插法插入,所以顺序相反。但是此时原链表并没有变化,所以读线程不会受到
干扰。
- 统计size
因为统计size的过程中,个数可能会变化。所以ConcurrentHashMap先尝试几次不加锁的方法统计每个Segment的大小,若在统计的过程中,数量发生变化,则再
采用加锁的方式统计。在putremove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。
ConcurrentHashMap 的高并发性主要来自于三个方面:
1.用分离锁实现多个线程间的更深层次的共享访问。
2.用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
3.通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。
ConcurrentHashMap 和 Hashtable区别
-
Hashtable锁住的是整张表,一个线程put时,其他线程不能put并且不能get
-
ConcurrentHashMap锁住的是某个Segment,并且支持并发操作。
-
ConcurrentSkipListMap
相当于线程安全的TreeMap
,继承与AbstractMap类,并且实现了ConcurrentNavigateMap接口
- Queue BlockingQueue阻塞队列,主要作为线程同步的工具,
在队列为空时,消费者的线程会等待队列变为非空。当队列满时,生产者线程会等待队列可用。所以常用于生产者消费者场景
BlockingQueue有两个阻塞方法
-
put(E e) 在队列尾部插入元素,若队列已满,则阻塞该线程
-
take() 从BlockingQueue头部取出元素,若队列为空,则阻塞该进程
抛出异常 不同返回值 阻塞线程 超时退出
队尾插入元素 add(e) offer(e) put(e) offer(e,time,unit)
队头删除元素 remove(e) poll() take() poll(time,unit)
获取、不删除元素 element() peek() --- ----
-
抛出异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。
当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常 。 -
返回特殊值:插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null
-
阻塞线程:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。
-
超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。
-
ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列。
-
LinkedBlockingQueue 是单向链表实现的(指定大小)阻塞队列,该队列按 FIFO(先进先出)排序元素。
-
LinkedBlockingDeque是双向链表实现的(指定大小)双向并发阻塞队列,该阻塞队列同时支持FIFO和FILO两种操作方式。
-
ConcurrentLinkedQueue是单向链表实现的无界队列,该队列按 FIFO(先进先出)排序元素。
-
ConcurrentLinkedDeque是双向链表实现的无界队列,该队列同时支持FIFO和FILO两种操作方式。
文件
- File
String path = '';
File f = new File(path); //读取一个文件
//File 常用方法
f.createNewFile() //创建文件
f.getAbsolutePath() //文件绝对路径
f.exists() //文件是否存在
f.isDirectory() //是否为目录
f.isFile() //是否为文件
f.length() //文件长度
f.lastModified() //最后修改时间
// 以字符串数组的形式,返回当前文件夹下的所有文件(不包含子文件及子文件夹)
f.list();
// 以文件数组的形式,返回当前文件夹下的所有文件(不包含子文件及子文件夹)
File[]fs= f.listFiles();
// 以字符串形式返回获取所在文件夹
f.getParent();
// 以文件形式返回获取所在文件夹
f.getParentFile();
/a/b/c
// 创建文件夹,如果a b 文件夹不存在,创建失败
f.mkdir();
// 创建多级文件夹,如果a b 文件夹不存在,都会创建
f.mkdirs();
// 创建一个空文件,如果父文件夹不存在,就会抛出异常
f.createNewFile();
// 所以创建一个空文件之前,通常都会创建父目录
f.getParentFile().mkdirs();
// 列出所有的盘符c: d: e: 等等
f.listRoots();
// 刪除文件
f.delete();
// JVM结束的时候,刪除文件,常用于临时文件的删除
f.deleteOnExit();
- 字节流 常用读取二进制文件,如图片、影像等
- 输入流抽象基类 InputStream 实现类 FileInputStream
File f = new File();
FileInputStream fis = new FileInputStream(f);
byte[] all = new byte[(int)f.length()];
fis.read(all); //从输入流中读取数据 ,一次读多个字节
for (byte bt:all
) {
System.out.println(bt);
}
fis.close();
try {
System.out.println("以字节为单位读取文件内容,一次读一个字节:");
in = new FileInputStream(file);
int tempbyte;
while ((tempbyte = in.read()) != -1) { // 一次读一个字节
System.out.write(tempbyte);
}
in.close();
} catch (IOException e) {
e.printStackTrace();
return;
}
- 输出流抽象基类 OutputStream 实现类 FileOutputStream
byte[] data={};
File f = new File();
FileOutputStream fos = new FileOutputStream(f); //输出流输出文件时,若文件不存在则会创建文件,文件存在会被覆盖,目录不存在会抛出异常
FileOutputStream fo = new FileOutputStream(f,true); //第二个参数设置为true时,也表示为追加文件
fos.write(data);
- 流的关闭
- 流在try中关闭的话,如果文件不存在或者流读取的时候出现问题,就会抛出异常,而关闭流就不会执行,所以应该在finally中进行关闭
File f = new File();
FileInputStream fis = null; //流的声明在try外面,否则finally读取不到
try{
fis = new FileInputStream(f);
...
}catch(IOException e){
}finally{
if(null != fis){ //关闭之前,判断是否实例化
try{
fis.close();
}catch(IOException e){
e.prinStackTrace();
}
}
}
上面的关闭方式为try-with-resources。而所有的流都实现了一个接口叫做AutoCloseable,任何类实现了这个接口,都可以在try()中进行实例化。 并且在try, catch, finally结束的时候自动关闭,回收相关资源。
try (FileInputStream fis = new FileInputStream(f)) {
byte[] all = new byte[(int) f.length()];
fis.read(all);
for (byte b : all) {
System.out.println(b);
}
} catch (IOException e) {
e.printStackTrace();
}
- 字符流 常用于读取普通文件,文本数字等
- 字符输入流 基类 Reader 实现类FileReader
try(FileReader fr = new FileReader(f)){ //字符流读入
char[] res = new char[(int)f.length()];
fr.read(res); //读入一个字符数组
for (char ch:res
) {
System.out.println(ch);
}
}catch (IOException e){
e.printStackTrace();
}
- 字符输出流 基类Writer 实现类FileWriter
// 打开一个写文件器,构造函数中的第二个参数true表示以追加形式写文件
FileWriter writer = new FileWriter(fileName, true);
try(FileWriter fw = new FileWriter(f2)){
String s = "abcde";
fw.write(s); //可以写入一个字符数组或者字符串
}catch (IOException e){
e.printStackTrace();
}
- 缓存流
-
字节流和字符流每一次读取都要访问硬盘,效率低下。而缓存流在读取一次读取较多的数据到缓存中,后面再读取就到缓存中读取,直到缓存中无数据,然后再到硬盘中读取
而缓存流在写入的时候,会先把数据写入到缓存区,直到缓存去中数量满了,才一次全部写入硬盘中,减少了IO操作。 -
缓存字符输入流 BufferedReader
try{
FileReader fr = new FileReader(f);
BufferedReader bf = new BufferedReader(fr); //缓冲读入流,必须建立在一个已存在的流基础上
String line;
while((line = bf.readLine()) != null){ //一次读取一行
System.out.println(line);
}
br.close();
fr.close(); //先关闭缓存流,再关闭文件流
}catch (IOException e){
e.printStackTrace();
}
- 缓存字符输出流 BufferedWriter 缓存流调用flush()可以立即把数据写入硬盘
try{
FileWriter fw = new FileWriter(f2);
BufferedWriter bw = new BufferedWriter(fw); //缓存流建立在一个已存在的流的基础上
bw.write("aaa");
bw.newLine(); //BufferedWriter 写入时不会自动换行,需要手动添加
bw.write("bbbb");
bw.close();
fw.close();
}catch (IOException e){
e.printStackTrace();
}
而PrintWriter可以自动换行
try{
FileWriter fw = new FileWriter(f2);
PrintWriter pw = new PrintWriter(fw);
pw.println("aaa");
pw.close();
fw.close();
}catch (IOException e){
e.printStackTrace();
}
- 字节流向字符流转换 InputStreamReader OutputStreamWriter
File f = new File();
FileInputStream fi = new FileInputStream(f);
InputStreamReader inReader = new InputStreamReader(fi);
BufferedReader br = new BufferedReader(inReader);
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(file, true)));
网络TCP
- 客服端
1. 指定服务器地址和端口,建立Socket
2. 获取输出流,把数据变成字节数组,通过输出流发送给服务端
3. 关闭输出流,获取输入流,获取反馈信息
4. 关闭资源
import java.io.*
import java.net.*
public void main(){
Socket s = null;
try{
if(s == null){
s = new Socket("xxx",13000);
byte[] buf = "abcdef".getBytes(); //把数据转换成字节数组
OutputStream out = s.getOutputStream(); //获取输出流
out.write(buf); //发送数据
s.shutdownOutput(); //关闭发送流
InputStream in = s.getInputStream(); //获取输入流,获取反馈信息
byte[] buffer = new byte[1024];
int len = in.read(buffer);
System.out.println(new String(buffer,0,len)); //
}
}catch (Exception e){
e.printStackTrace();
}finally {
if(s!=null){
try{
s.close();
}catch (Exception e){
}
}
}
}
- 服务器端
1. 建立服务ServerSocket服务,然后,用ServerSocket的accept()方法得到socket服务
2. 获取输入流,然后可以得到数据
3. 反馈信息给客户端
4. 关闭资源
InetAddress ia = InetAddress.getByName("www.baidu.com"); //获得域名的IP
String ip = ia.getHostAddress();
System.out.println(ip);
ia = InetAddress.getLocalHost();
ip = ia.getHostAddress();
System.out.println(ip);
ServerSocket ss = new ServerSocket(13000);
Socket s = ss.accept();
String clientip = s.getInetAddress().getHostAddress(); //获取IP地址
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int len = 0;
while((len = in.read(buf))!= -1){
System.out.println(new String(buf,0,len));
}
s.shutdownInput();
/*
输入流转换为字符流
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
while((info = br.readLine()) != null){
syso(info);
}
*/
OutputStream out = s.getOutputStream(); //发送反馈信息
out.write("server has get".getBytes());
/*
输出流转字符流
PrintWriter pw = new PrintWriter(s.getOutputStream,true);
pw.println("hello world");
*/
s.close();
ss.close();
反射
- 获取类对象 以下三种方式都可以获取类对象 一中类只有一个类对象 此时类的静态属性也会被加载
- Class.forName
- Hello.class
- new Hello().getClass()
- 反射创建一个对象
1. 先获取类对象
2. 通过类对象获取该类的构造器对象
3. 再通过一个构造器创建一个对象
Class c1 = Class.forName(name);
//反射机制创建对象
Constructor c = c1.getConstructor(); //获取构造器,这中是无参的构造方法,可以传入类对象,调用其他构造方法
Hello h = (Hello)c.newInstance(); //获取实例
h.setI(1000);
System.out.println(h.getI());
- 获取对象字段
Class c1 = Class.forName(name);
//反射机制创建对象
Constructor c = c1.getConstructor(); //获取构造器
Hello h = (Hello)c.newInstance(); //获取实例
Filed f1 = c1.getDeclaredField("name"); //获取name字段
f1.set(h,"jmy"); //设置字段值
System.out.println(h.getI());
getFiled()只能获取public字段,包括从父类继承来的字段
getDeclaredField()可以获取本类的所有字段,包括private的,但是不能获取继承来的字段,虽然可以获取private字段,但是无法访问该值,可以使用
f1.setAccessible(true)来暴力反射私有字段。
- 调用方法
Class c1 = Class.forName(name);
//反射机制创建对象
Constructor c = c1.getConstructor(); //获取构造器
Hello h = (Hello)c.newInstance(); //获取实例
//第一个参数为要调用的方法名,第二个是该方法的参数类型
//第二类型参数是类对象,如果是对象就使用.class ,比如String.class, test.class
//如果是基本类型,就是用int.class 等价于Integer.TYPE, 其他基本类型类似
Method m = c1.getMethod("setI",Integer.TYPE);
m.invoke(h,200); //调用该方法
System.out.println(h.getI());
- 反射作用
反射在Spring的IOC(控制反转,依赖注入)中有很大作用。如果不用反射,当调用业务方法一时,编写调用业务方法1的代码,
当调用业务方法二时,编写调用业务方法1的代码。当使用反射,把类的名称和要调用的方法写入配置文件即可,当需要调用其他类时,只要修改
配置文件中的类名和方法名即可,而不用修改代码。
比如配置文件
class=reflection.Service1
method=doService1
读取配置文件,进行调用
File config = new File("xx.txt");
Properties pro = new Properties();
pro.load(new FileInputStream(config));
String className =(String)pro.get("class");
String methodName = (String)pro.get("method");
Class cas = Class.forName(className); //根据类名称获取类对象
Method mt = cas.getMethod(methodName); //根据方法名称,获取方法
Constructor ct = cas.getConstructor(); //获取构造器
Object obj = ct.newInstance(); //根据构造器,实例化对象
mt.invoke(obj); //调用对象指定方法