0%

数据结构

定义

数据结构就是一种程序设计优化方法论,研究数据的“逻辑结构“和”物理结构”以及它们之间相互关系,并对这种结构定义相应的”运算,目的是加快程序的执行速度、减少内存的占用空间

研究对象

研究对象1:数据之间的逻辑关系

  • 集合结构

  • 线性结构:一对一

  • 树形结构:一对多

  • 图形结构:多对多

研究对象2:数据的存储结构

  • 顺序结构
  • 链式结构
  • 索引结构
  • 散列结构

开发中,更习惯于如下的方式理解存储结构

  • 线性表(一对一):一维数组、单向链表、双向链表、栈、队列
  • 树(一对多):各种树。 二叉树、B+树
  • 图(多对多):
  • 哈希表:HashMap、HashSet

研究对象3:相关算法操作

  • 分配资源、建立结构、释放资源

  • 插入和删除

  • 获取和遍历

  • 修改和排序

存储结构

数组

链表

单向链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Node{

Object data;

Node next;

public Node(Object data){
this.data = data;
}
}

Node node1 = new Node("AA");
Node node2 = new Node("BB");
node1.next = node2;

双向链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Node{
Node prev;
Object data;
Node next;

public Node(Object data){
this.data = data;
}

public Node(Node prev, Object data, Node next){
this.prev = prev;
this.data = data;
this.next = next;
}
}


Node node1 = new Node(null, "aa", null);
Node node2 = new Node(node1, "bb", null);
Node node3 = new Node(node2,"cc", null);

node1.next = node2;
node2.next = node3;

二叉树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class TreeNode{
TreeNode parent;
TreeNode left;
Object data;
TreeNode right;

public TreeNode(Object data){
this.data = data;
}
public TreeNode(TreeNode left, Object data, TreeNode right){
this.left =left;
this.data = data;
this.right=right;
}
}

TreeNode node1 = new TreeNode(null, null,"AA", null);
TreeNode leftNode = new TreeNode(node1, null,"bb", null);
TreeNode rightNode = new TreeNode(node1, null,"cc", null);
node1.right = rightNode;
node1.left = leftNode;

栈FILO

  • 属于抽象数据类型(ADT)
  • 可以使用数组或链表来构建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Stack{
//数组实现栈
Object[] values
int size;

public Stack(int length){
values = new Object[length];
}
public void push(Object ele){
if(size>=values.length){
throw new RuntimeException("栈空间已满")
}
values[size]=ele;
size++;
}
public Object pop(){
if(size <=0){
throw new RuntimeException("栈空间已空")
}
Object obj = values[size-1];
values[size-1] = null;
size--;
return obj;

}
}

队列FIFO

  • 属于抽象数据类型(ADT)
  • 可以使用数组或链表来构建

class Stack{
//数组实现栈
Object[] values
int size;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Queue{
Object[] values;
int size;

public Queue(int lenght){
values = new Object[length];
}

public void add(Object ele){
if(size>= values.length){
throw new RuntimeException("队列已满,添加失败");
}
values[size] = ele;
size ++;
}

public Object get(){
if(size<=0){
throw new RuntimeException("队列已空,获取失败");
}

Object obj = values[0];
for(int i= 0;i<size-1;i++){
values[i] = values[i+1];
}
values[size - 1] = null;
size --;
return obj;
}
}

List源码

ArrayList

ArrayList特点

  • 实现了List接口,存储有序、可以重复的数据
  • 底层使用Object[]数组存储
  • 线程不安全

ArrayList源码解析

jdk7: 如下代码执行:底层会初始化数组,数组的长度为10。Object[] elementData = new Object[10]

ArrayList list = new ArrayList<>()

list.add(“AA”);//elementData[0] = “AA”;

list.add(“BB”); //elementData[1] = “BB”;

…..

当添加第11个元素时,底层elementData数组已满,则需要扩容。默认扩容为原来长度的1.5倍。并将原有数组中的元素赋值到新的数组中

**jdk8:**底层会初始化数组,即:Object[] elementData= new Object[]{};

ArrayList list = new ArrayList<>();

list.add(“AA”);//首次添加元素,会初始化数组 elementData = new Object[10]; elementData[0] = “AA”;

list.add(“BB”);//elementData[1] = “BB”;

当添加第11个元素时,底层elementData数组已满,则需要扩容。默认扩容为原来长度的1.5倍。并将原有数组中的元素赋值到新的数组中

小结

jdk7中类似于饿汉式

jdk8中类似于懒汉式

Vector

特点

  • 实现了List接口,存储有序、可以重复的数据
  • 底层使用了Object[]数组存储
  • 线程安全

源码

Vector v = new Vector();//底层初始化数组,长度为10 Object[] element = new Object[10];

v.add(“aa”);

v.add(“bb”);

当前添加第11个元素时,扩容2倍

LinkedList

特点

  • 实现List接口,存储有序、可以重复的数据
  • 底层使用了双向链表存储
  • 线程不安全

源码

LinkedList list = new LinkedList<>(); //底层也没做啥

list.add(“AA”);//将“AA”封装到一个Node对象中,list对象的属性first、last都指向次node对象。

list.add(“BB”);//将“BB”封装到一个Node对象2中,对象1和对象2构成一个双向链表,同时last指向此Node对象2

…..

因为linkedList使用的是双向链表,不需要考虑扩容问题。

启示

1.Vector不使用了

2.ArrayList底层使用数组结构,查找和添加(尾部添加)操作效率高,时间复杂度为O(1)

删除和插入操作效率低,时间复杂度为O(n)

LinkedList底层使用双向链表,删除和插入操作效率高,时间复杂度O(1)

查找和添加(尾部添加)操作效率低,时间为O(n)(添加可能O(1))

3.在选择了ArrayList的前提下,new ArrayList():底层创建长度为10的数组。

new ArrayList(int capacity):底层创建指定capacity长度的数组。

Map源码

HashMap源码解析

jdk7:

//创建对象的过程中,底层会初始化数组Entry[] table = new Entry[16];

HashMap<String, Integer> map = new HashMap<>();

map.put(“AA”, 78); //“AA”和78封装到一个Entry对象总,考虑将此对象添加到table数组中。

添加/修改的过程:

将(key1,value1)添加到当前的map中

首先,需要调用key所在类的hashCode()方法,计算key1对应的哈希值1,此哈希值1经过某种算法(hash())之后,得到哈希值2。

哈希值2再经过某种算法(indexFor())之后,就确定了(key1,value1)在数组table中的索引位置i。

  • 如果此索引位置i的数组上没有元素,则(key1,value1)添加成功 —>情况1
  • 如果此缩影位置i的数组上有元素(key2,value2),则需要继续比较key1和key2的哈希值2 —>哈希冲突
    • 如果key1的哈希值2与key2的哈希值2不相同,则(key1,value1)添加成功。 —->情况2
    • 如果key1的哈希值2与key2的哈哈希值2相同,则需要继续比较key1和key2的equals()。要调用key1所在类的equals(),将key2作为参数传递进去
      • 调用equals(),返回false:则(key1,value1)添加成功 —->情况3
      • 调用equals(),返回true:则认为key1和key2是相同的。默认情况下,value1替换原有的value2.

说明

  • 情况1:将(key1, value1)存放到数组的索引i的位置
  • 情况2,情况3:(key1,value1)元素与现有的(key2, value2)构成单向链表结构,(key1,value1)指向(key2,value2)

随着不断的添加元素,在满足如下的条件的情况下,会考虑扩容:

(size>= threshold) &&(null !=table[i])

当元素的个数达到临界值(->数组的长度* 加载因子)时,就考虑扩容。 默认的临界值 = 16*0.75 –》12.

默认扩容为原来的两倍

jdk8与jdk7的不同之处

  1. 在jdk8中,当我面创建了HashMap实例以后,底层并没有初始化table数组。当首次添加(key,value)时,进行判断,如果发现table尚未初始化,则对数组进行初始化。
  2. 在jdk8中,HashMap底层定义类Node内部类,替换了jdk7中的Entry内部类。意味着,我们创建的数组是Node[]
  3. jdk8中,当前的(key,value)经过一系列判断之后,可以添加到当前的数组的角标i中。如果此时角标i位置上有元素。在jdk7中是将新的(key,value)指向已有的旧的元素(头插法)。而在jdk8中旧的元素指向新的(key,value)元素(尾插法)
  4. jdk7:数组+单向链表;jdk8:数组+单向链表+红黑树。

​ 什么时候会使用红黑树:如果数组索引i位置上的元素的个数达到8,并且数组的长度达到64时,我们就将此索引i位置上的多个元素改为使用红黑树的结构进行存储。 (为什么修改呢?红黑树进行put()/get()/remove(),操作的时间复杂度为O(logn),比单向链表的时间复杂度O(n)的好。性能高。

​ 什么时候会使用红黑树变为单向链表:当使用红黑树的索引i位置上的元素的个数小于6时,就会将红黑树退化为单向链表。

LinkedHashMap

LinkedHashMap是HashMap的子类

使用的数组+单向链表+红黑树的基础上,又增加了一对双向链表,记录添加的(key,value)的先后顺序。便于我们遍历所有的key-value。

重写了增加方法。

HashSet和LinkedHashSet源码分析

  • HashSet底层使用HashMap
  • LinkedHashSet使用的是LinkedHashMap

常见问题

链表和数组的区别?

栈是如何运行的?

ArraList的默认大小是多少,以及扩容机制

ArraList的底层是怎么实现的

数组和ArrayList的区别

ArrayList看作是对数组的常见操作的封装

什么是线程安全的List

HashMap初始值16,临界值12是怎么算的

HashMap长度为什么是2的幂

为了方便计算要添加的元素的底层的索引i

HashMap怎么计算哈是在和索引?扩容机制?解决Hash冲突

HashMap底层为什么加链表?

因为有哈斯冲突。解决方案,使用链表的方式。保证要添加的元素仍然在索引i的位置上

HashMap为什么长度达到一定的长度要转化为红黑树?

HashMap的get()原理?

hashCode()与equals()生产算法,方法怎么重写?

进行equals()判断使用的属性,通常也都会参与到hashCode()的计算中。尽量保证hashCode()的一致性。

equals与==的区别,equals相等hash值一定相等吗?hash值相等equals一定相等吗?

泛型的引入

所谓泛型,就是允许在定义类、接口时通过一个标识来表示类中某个属性的类型或者是某个方法的返回值或参数的类型。这个类型参数将在使用时(例如,继承或实现这个接口、创建对象或调用方法时)确定(即传入实际的类型参数,也称为类型实参)

在集合使用泛型前可能存在的问题

问题1:类型不安全。因为add()的参数是Object类型,意味着任何类型的对象都可以添加成功

问题2:需要使用强转操作,繁琐。还有可能导致ClassCastException异常

泛型的使用

使用说明

  • 集合框架在生命接口和实现类时使用泛型,在实例化集合对象时,如果没有使用泛型,则认为操作的是Object类型的数据。如果使用了泛型,则需要指明泛型的具体类型。一旦指明了泛型的具体类型,则阿紫集合的相关的方法中,凡是使用泛型的位置,都替换为具体的泛型类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class GenericTest {
@Test
public void test(){
List<Integer> list = new ArrayList<Integer>();
list.add(78);
list.add(254);
list.add(54);
list.add(99);
//编译报错,保证类型安全
// list.add("AA");
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
//因为添加的都是Integer类型,避免了强转操作
Integer i = iterator.next();
int score = i;
System.out.println(score);
}
}
//在map中的使用
@Test
public void test2(){
HashMap<String, Integer> map = new HashMap<>();//类型推断
map.put("Tom", 12);
map.put("jack",23);
map.put("hh", 24);

Set<Map.Entry<String, Integer>> entreSet = map.entrySet();
Iterator<Map.Entry<String, Integer>> iterator = entreSet.iterator();

while (iterator.hasNext()){
Map.Entry<String, Integer> entry = iterator.next();
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key+"-->"+value);
}
}
}

自定义泛型

自定义泛型类\接口

格式

class A{

}

interface B<T1,T2>{

}

使用说明:

  1. 声明完自定义泛型类以后,可以在类的内部(比如属性、方法、构造器中)使用类的泛型
  2. 创建自定义泛型类的对象时,可以指明泛型参数类型。一旦指明,内部凡是所使用类的泛型参数的位置,都具体化为指定的类的泛型类型。
  3. 创建自定义泛型类的对象时,没有指明泛型参数类型,那么泛型将被擦除,泛型对应的类型均安装Object处理,但不等于Object。经验:泛型要用使用一路都用,要不用,一路都不用。
  4. 泛型的指定中必须使用引用数据类型。不能使用基本数据类型,此时只能使用包装类替换。
  5. 除创建泛型类对象外,子类继承泛型类时、实现类实现泛型接口时,也可以确定泛型结构中的泛型参数。

如果我们在给泛型类提供子类时,子类也不确定泛型的类型,则可以继续使用泛型参数。

我们还可以再现有的父类的泛型参数的基础上,新增泛型参数

注意:

  1. 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如<E1,E2,E3>

  2. JDK7开始,泛型的简化操作:ArrayList flist = new ArrayList<>();

  3. 如果泛型结构是一个接口或抽象类,则不可以创建泛型类的对象

  4. 不能使用new E()。但是可以,E[] elements = (E[])new Object[capacity];

  5. 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,但不可以在静态方法中使用类的泛型。

  6. 异常类不可以带泛型。

自定义泛型方法

格式

权限修饰符 返回值类型 方法名(形参列表){

}

举例

public E method(E e){

}

说明

  • 声明泛型方法时,一定要添加泛型参数
  • 泛型参数在方法调用时,指明其具体的类型
  • 泛型方法可以根据需要声明为static
  • 泛型方法所属的类是否是一个泛型类,都可以。

泛型在继承上的体现

1.类SuperA是类A的父类,则G与G的关系?

二者是并列的两个类,没有任何子父类的关系

2.类SuperA是类A的父类或接口,superA与A的关系?

superA与A有继承或实现的关系。即A的实例可以赋值给SuperA类型的引用或变量。

通配符

使用说明

ArrayList<?>

G 可以看作是G类型的父类,即可以将G的对象赋值给G类型的引用或变量

读写特点

  • 读取数据:允许的,读取的值为Object类型
  • 写入数据:不允许。特例,null

有限制条件的通配符

List<? extends A>:可以将List或List赋值给List<? extends A>其中B类是A类的子类

List<? super A>:可以将List或List赋值给List<? super A>其中B类是A类的父类

常见问题

Java的泛型是什么?有什么好处和优点?JDK不同版本的泛型有什么区别?

泛型,是程序中出现的不确定的类型。

以集合来举例:把一个集合中的内容限制为一个特定的数据类型,这就是generic背后的核心思想。

引入

数组存储的特点

  • 数组一旦初始化,长度就确定了

  • 数组中的多个元素是依次紧密排列的,有序的,可重复的。

  • 数组一旦初始化完成,其元素的类型就是确定的。不是此类型的,就不能添加到此数组中

  • 元素的类型可以是基本数据类型,也可以是引用数据类型

反向来看

  • 一旦初始化,长度就不可变了
  • 数组中存储元素特点的单一性。对于无须、不可重复的场景的多个数据就无能为力了
  • 数组中可用的方法、属性都很少。具体需要都需要自己来组织相关代码逻辑
  • 对于数组中元素的删除、插入操作,性能较差。

Java集合框架体系

1.java.util.Collection:存储一个个的数据

  • 子接口:List:存储有序的、可重复的数据
    • **ArrayList(主要)**、LinkedList、Vector
  • 子接口:**Set:**存储无序的、不可重复的接口
    • **HashSet(主要)**、LinkedHashSet、TreeSet

2**.java.util.Map:**存储一对一对的数据

  • **HashMap(主要)**、LinkedHashmap、TreeMap、Hashtable、Properties

Collection

常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Test
public void test1(){
//add
Collection coll = new ArrayList();
coll.add("aaa");
coll.add(123);//自动装箱
coll.add(new Object());
System.out.println(coll);

//addAll()
Collection coll1 = new ArrayList();
coll1.add("bbb");
coll1.add("fff");
//size()
System.out.println(coll.size());
coll.addAll(coll1);
System.out.println(coll);

//isEmpty()
System.out.println(coll.isEmpty());
//contains(Object obj)
System.out.println(coll.contains("aaa"));
//containsAll(Collection coll)
Collection coll3 = new ArrayList();
coll3.add("xx");
coll3.add("haha");
System.out.println(coll.containsAll(coll3));
// clear 清空集合元素
//remove(Object):从集合中删除第一个找到的与obj对象equals返回true的元素
//removeAll(Collection coll):从当前集合中删除所有与coll集合中相同的元素.即this
//retainAll)Collection coll):当当前集合中删除两个集合中不同的元素


//集合 --》数组
Object[] arr = coll.toArray();
System.out.println(Arrays.toString(arr));

//数组--》集合
String[] arr2 = new String[]{"aa","bb"};
Collection list = Arrays.asList(arr2);
System.out.println(list);
}

添加元素的要求:

要求元素所属的类一定要重写equals()

迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void test(){
Collection coll = new ArrayList();
coll.add("aaa");
coll.add(123);
coll.add(new Object());

//获取迭代器对象
Iterator iterator = coll.iterator();
//实现遍历方式1:
System.out.println(iterator.next());
System.out.println(iterator.next());
System.out.println(iterator.next());
//实现遍历方式2:
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}

foreach循环

格式:

for(要遍历的集合或数组元素的类型 临时变量: 要遍历的集合或数组变量){

​ 操作临时变量的输出

}

针对于集合来讲,增强for循环的底层仍然使用的是迭代器

增强for循环的执行过程中,是将集合或数组中的元素一次赋值给临时变量,注意循环体中对临时变量的修改,可能不会导致原有集合或数组中元素的修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test2() {
Collection coll = new ArrayList();
coll.add("aaa");
coll.add(123);
coll.add(new Object());

for (Object obj : coll) {
System.out.println(obj);
}

int[] arr = new int[]{1,2,3,4,5};
for (Object i : arr) {
System.out.println(i);
}
}

List(Collection)

常用方法

1.collections中含有的15个方法

2.由于集合是有序的,进而就有与索引相关的一些方法

插入元素:

  • void add(int index, Object ele)

  • boolean addAll(int index, Collection eles)

获取元素

  • Object get(int index):获取指定index位置的元素
  • List subList(int fromIndex, int toIndex)返回从fromIndex到toIndex的位置的子集合

获取元素索引

  • int indexOf(Object obj):返回obj在集合中首次出现的位置
  • int lastIndexOf(Object obj):返回Obj在当前集合中末次出现的位置

删除和替换元素

  • Object remove(int index):移除指定index位置的元素,并返回此元素
  • Object set(int index, Object ele):设置指定index位置的元素为ele

小结:

:add(Object obj) addAll(Collection coll)

: remove(Object obj) remove(int index)

:set(int index, Object ele)

:get(int index)

:add(int index, Object ele) addAll(int index, Collection eles)

长度: size()

遍历:iterator(), foreach, for

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test1(){
List list = new ArrayList();
list.add("AA");
list.add(123);
list.add("bb");
System.out.println(list.toString());

list.add(2,"Cc");
List list1 = Arrays.asList(1,2,3);

// list.add(1,list1); //将list1看作一个整体加入list
list.addAll(1,list1);
System.out.println(list);

list.remove(2);//删除索引为2的元素
list.remove(Integer.valueOf(2));//删除数据为2

}

实现类及其特点

ArrayList: List的主要实现类:线程不安全、效率高;底层使用Object[]数组存储。添加、查询数据时效率高;插入、删除时,效率较低

LinkedList: 底层使用双向链表进行存储。在对集合中的数据频繁进行删除和插入时建议使用此类。插入、删除时,效率高,添加、查询数据时效率低。

Vector:List的古老实现类:线程安全、效率低;底层使用Object存储数据

Set(Collection)

无序性、不可重复性的理解

无序性: 不是随机性。也不是添加元素的顺序和遍历元素的顺序的不一致。而是与添加元素位置有关,不像ArrayList一样是依次紧密排列的,而是根据添加元素的哈希值,计算的其在数组中存储的位置。此位置不是依次排列的,表现为无序性

不可重复性:添加到Set中的元素是不能相同的。比较的标准,需要判断hashCode()得到的哈希值与equals()得到的结果。

常用方法:

即为Collection中声明的15个抽象方法。没有新增的方法

开发场景

较List、Map来说,set使用的频率较少

实现类:

**HashSet:**主要实现类;底层使用HashMap,即使用数组+单向链表+红黑树结构进行存储

LinkedHashSet: 是HashSet的子类。在现有数组+单向链表+红黑结构基础上,又添加了一组双向链表,用于记录添加元素的先后顺序。即:我们可以按照添加元素的顺序实现遍历。便于频繁的查询操作。

TreeSet:底层使用红黑树存储。可以按照添加的元素的指定的属性的大小顺序进行遍历

添加到HashSet/LinkedHashSet中元素的要求:

要求元素所在的类要重写两个方法:equals()和hashCode().

同时要求equals()和hashCode()要保持一致性

TreeSet

向TreeSet中添加元素的要求

添加到TreeSet中的元素必须是同一个类型的对象,否则会报ClassCastException.

判断数据是否相同的标准:

  • 不再是考虑hashCode()和equals()方法了,也就意味着添加到TreeSet中的元素不再需要重新hashcode和equals()

  • 比较元素大小的或比较元素是否相等的标准就是考虑自然排序或定制排序中compareTo()或compare()的返回值。如果compareTo()或compare()的返回值为0,则认为两个对象是相等的。由于TreeSet中不能存放相同的元素,则后一个相等的元素就不能添加到TreeSet中。

Map

实现类及对比

  • HashMap:主要实现类;线程不安全,效率高;可以添加null的key和value的值;底层使用数组+单向链表+红黑树结构存储
    • LinkedHashMap:是HashMap的子类,在HashMap使用的结构基础上,增加了一对双向链表,用于记录添加的元素的先后顺序。遍历的时候可以按照添加的顺序显示。开发中,对于频繁的遍历操作,建议使用此类。
  • Hashtable:古老实现类;线程安全,效率低;不可以添加null的key和value值;底层使用数组+单向链表结构存储
  • TreeMap:底层使用红黑树存储;可以安装添加的key-value中key元素的指定的属性的大小顺序进行遍历。需要考虑1.自然排序 2.定制排序
    • Properties:其key和value都是String类型。常用来处理属性文件。

HashMap中元素的特点

  • HashMap中的所有的key彼此之间是不可以重复的、无序的。所有的key就构成了一个Set集合。 ==》key所在的类要重写hashCode()equals()
  • HashMap中的所有的value彼此之间是可重复的、无序的。所有的value就构成一个Collection集合。==》value所在的类要重写equals()
  • HashMap中的一个key-value就构成了一个Entry。
  • HashMap中所有的entry彼此之间是不可重复的、无序的。所有的entry就构成了一个set集合。

常用方法

添加、修改操作:

​ Object put(Object key, Object value):将指定key-value添加到当前map对象中

​ void putAll(Map m):将m中所有key-value对存放到当前map中

删除

​ Object remove(Object key) 移除指定key的key-value对,并返回value

​ void clear():清空当前map中所有的数据

元素查询

​ Object get(object key)获取指定key对应value

​ boolean containKey(Object key)是否包含指定的key

​ boolean containsValue(Object value):是否包含指定的value

​ int size():返回map中key-value对的个数

​ boolean isEmpty():判断当前map是否为空

​ boolean equals(Object obj):判断当前map和参数对象是否相等

元视图操作的方法

  • Set keySet() 返回所有key构成的Set集合
  • Collection values():返回所有value构成的Collection集合
  • Set entrySet():返回所有key-value对构成的Set集合

小结:

增:put(Object key, Object value), putAll(Map m)

删:Object remove(Object key)

改:put(Object key, Object value), putAll(Map m)

查:Object get(Object key)

长度:size()

遍历:

​ 遍历key集: Set keySet()

​ 遍历value集: Collection values()

​ 遍历entry集:Set entrySet()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
    @Test
public void test1(){
//put(Object key, Object value)
HashMap map = new HashMap();
map.put("AA", 56);
map.put(65,"TOM");
map.put("BB", 78);
System.out.println(map);

//size()
System.out.println(map.size());

//Object remove(Object key)
Object value = map.remove("AA");
System.out.println(map);

//修改:put
Object oldValue = map.put("BB",99);
System.out.println(oldValue);
System.out.println(map);

//get
Object value1 = map.get("BB");
System.out.println(value1);


//遍历key集
Set keySet = map.keySet();
//使用迭代器
Iterator iterator = keySet.iterator();
while(iterator.hasNext()){
Object key = iterator.next();
System.out.println(key);
}

//遍历Value集:
Collection values = map.values();
//方式1:for-each
for(Object obj: values){
System.out.println(obj);
}
//方式2:
Set keySet1 = map.keySet();
for(Object key:keySet1){
Object value2 = map.get(key);
System.out.println(value2);
}

//方式1:遍历entry集
Set entryset = map.entrySet();
Iterator iterator2 = entryset.iterator();
while(iterator.hasNext()){
//方法1
System.out.println(iterator.next());
//方法2
Map.Entry entry = (Map.Entry) iterator2.next();
System.out.println(entry.getKey()+"-->"+entry.getValue());

}

//方式2:遍历entry集
Set keySet2 = map.keySet();
for (Object key:keySet2){
System.out.println(key+ "--->"+map.get(key));
}
}
}

TreeMap的使用

  • 底层使用红黑树存储
  • 可以按照添加的key-value的key元素指定的属性大小顺序进行遍历
  • 需要考虑1.自然排序 2.定制排序
  • 要求向TreeMap中添加的key必须是同一个类型的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
public class TreeMapTest {
@Test
public void test1(){
//自然排序
TreeMap map = new TreeMap();
map.put("CC", 89);
map.put("BB",78);
map.put("jj",new Data());
map.put("kk",78);

Set entrySet = map.entrySet();
for(Object entry:entrySet){
System.out.println(entry);
}
}
@Test
public void test2(){
TreeMap map = new TreeMap();

User u1 = new User("Tom", 23);
User u2 = new User("Jerry",43);
User u3 = new User("Rose",13);
User u4 = new User("Jack",23);
User u5 = new User("Tony", 33);

map.put(u1,78);
map.put(u2,76);
map.put(u3,76);
map.put(u4,77);
map.put(u5,45);

Set entrySet = map.entrySet();
for(Object entry:entrySet){
System.out.println(entry);
}

}

@Test
public void test3(){
//定制排序
Comparator comparator = new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof User && o2 instanceof User) {
User u1 = (User) o1;
User u2 = (User) o2;

int value = u1.getName().compareTo(u2.getName());
if (value != 0) {
return value;
}
return u1.getAge() - u2.getAge();
}
throw new RuntimeException("类型不匹配");
}
};
TreeMap map1 = new TreeMap();
}
}


class User implements Comparable {
private String name;
private int age;

public User() {
}

public User(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
//定义自然排序的规则
public int compareTo(Object o) {
if(this == o){
return 0;
}
if(o instanceof User){
User u = (User) o;
int value = this.age - u.age;
if(value !=0){
return value;
}
return -this.name.compareTo(u.name);
}
throw new RuntimeException("类型不匹配");
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

Properties的使用

Properties是Hashtable的子类,其key和value都是String类型的,常用来处理属性文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PropertiesTest {
@Test
public void test() throws IOException {
//将数据封装到具体的配置文件中,在程序中读取配置文件中的信息,实现了数据和代码的解耦
File file = new File("info.properties");
System.out.println(file.getAbsolutePath());
FileInputStream fis = new FileInputStream(file);
Properties pros = new Properties();
pros.load(fis);//记载流中文件中的数据

//读取数据
String name = pros.getProperty("name");
String pwd = pros.getProperty("password");
System.out.println(name+"xx"+pwd);

fis.close();
}

}

Collections

Collections是一个操作Set,List和Map等集合的工具类。

**Question:**Collection和Collections

Collection:集合框架中的用于存储一个一个元素的接口,又为List和Set等子接口

Collections:用于操作集合框架的一个工具类。此时集合框架包括:Set、List、Map

常用方法

排序操作:

  • reverse(list):反转list中的元素

  • shuffle(list):对list集合元素进行随机排序

  • sort(list):根据元素的自然顺序对指定List集合元素按升序排序

  • sort(list.Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序

  • swap(list.int int):将指定list集合中i处元素和j处元素进行交换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void test(){
List list = Arrays.asList(45,34,64,23,23,12,11,24,5);
System.out.println(list);
// Collections.reverse(list);
Collections.shuffle(list);
Collections.sort(list);
Collections.sort(list, new Comparator(){
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof Integer && o2 instanceof Integer){
Integer i1 = (Integer) o1;
Integer i2 = (Integer) o2;
return -(i1- i2);
}
throw new RuntimeException("类型不匹配");
}
});
System.out.println(list);
}

查找:

  • Object max(Collection):根据元素自然排序,返回集合中最大的元素
  • Object max(Collection.Comparator):根据Comparator指定的顺序,返回给定集合中最大元素
  • Object min(Collection):根据元素的自然顺序,返回给定集合中最小的元素
  • Object min(Collection Comparator):根据Comparator指定的顺序,返回给定集合中最小元素
  • int binarySearch(List list, T key)在List集合中查找某个元素的小标。
  • int binarySearch(List list, T key,Comparator c)在List集合中查找某个元素的小标,
  • int frequency(Collection c, Object o):返回指定集合中指定元素出现的次数

复制:

  • void copy(List des List src):将src中的内容赋值到dest中

    1
    2
    3
    List src = Arrays.asList(45,34,64,23,23,12,11,24,5);
    List dest = Arrays.asList(new Object[src.size()]);
    Collections.copy(dest,src);
  • boolean replaceAll(List list, Object oldVal, Object newVal):使用新值替换List对象中所有旧值

  • boolean addAll(Collection c, T..elements)将所有制定元素添加到指定collection中

  • Collections类中提供了多个synchronizedXXX()方法,该方法可使将指定集合包装成线程同步的集合

常见问题:

List,Set,Map是否为Collection的接口,三者的区别?

list,map,set接口的实现类,并说出其特点

集合的父类是谁?哪些安全的?

遍历集合的方式有哪些?

迭代器iterator用来遍历Collection,不能用来遍历Map

增强for

ArrayList与LinkedList区别?

Set里的元素是不能重复的,那用什么来区分重复与否,用equals()

hashCode()、equals()

TreeSet两种排序方式在使用的时候怎么起作用?

在添加新的的元素时,需要调用compareTo或compare()

TreeSet的数据结构

红黑树

final怎么用,修饰Map可以继续添加数据吗?

Set和Map的比较

HashSet底层是HashMap

LinkedHashSet底层就是LinkedHashMap

TreeSet底层就是TreeMap

HashMap线程安全吗?

不安全。

Hashtable是怎么实现爱你的,为什么安全?

数组+单向链表;底层方法使用synchronized修饰

HashMap和TreeMap的区别

底层数据结果截然不同

HashMap里面实践装的是什么?

JDK7:HashMap内部声明了Entry,实现了Map中Entry接口(key,value作为Entry两个属性出现)

JDK8:HashMap内部声明了Node,实现了Map中Entry接口(key,value作为Entry两个属性出现)

HashMap的key存储在哪里?

数组+链表+红黑树

自定义类型可以作为Key吗?

可以。要重写hashCode()和equals方法

String类

类的声明

  • final:String是不可以被继承

  • Serializable:可序列化的接口。凡是实现此接口类的独显就可以通过网络或本地流进行数据的传输

  • Comparable:凡是实现此接口类,其对象都可以比较大小

内部声明的属性

jdk8:

private final char value[]:存储字符串数据的容器

​ final:指明此数组一旦初始化,其地址就不可变

jdk9节省内存空间

private final char value[]:存储字符串数据的容器

字符串常量的存储位置

  • 字符串常量都存储在字符串常量池(StringTable)中

  • 字符串常量池不允许存放两个相同的字符串常量

  • 字符串常量池,在不同的jdk版本中,存放位置不同

    jdk7前:字符串常量池在方法区

    jdk7之后:字符串常量池存放在堆空间

String不可变性的理解

  1. 当对字符串重新赋值时,需要重新制定一个字符串常量的位置进行赋值,不能在原有的位置修改
  2. 当对现有的字符串进行拼接操作时,需要重新开辟空间保存拼接以后的字符串,不能在原有的位置修改
  3. 当调用字符串的replace()替换现有的某个字符时,需要重新开辟空间保存修改以后的字符串,不能在原有的位置修改

String实例化的两种方式

String s1 = “hello”;

String s2 = new String(“hello”);

Question:

String s2 = new String(“hello”) 在内存中创建了几个对象?

两个。一个在堆空间中new的对象。另一个是在字符串常量池中生成的字面量。

String连接操作

Case1:常量+常量:结果仍存储在字符串常量池中.此时的常量可能是字面量,可能是final修饰的常量。

Case2: 常量+变量 或变量+变量:都会通过new的方式创建一个新的字符串,返回堆空间此字符串对象的地址

Case3:调用字符串的intern():返回的是字符串常量池中字面量的地址

Concat():不管常量调用此方法,还是变量调用,同样不管参数是常量还是变量,总之。调用完concat()方法都返回一个新new的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class StringDemo {
@Test
public void test1(){
Person p1 = new Person();
Person p2 = new Person();
p1.name = "Tom";
p2.name = "Tom";
}
@Test
public void test2(){
String s1 = "hello";
String s2 = "world";
String s3 = "helloworld";
String s4 = "hello" +"world";
String s5 = s1 +"world";
String s6 = "hello" + s2;
String s7 = s1 +s2;

System.out.println(s3 == s4); //true
System.out.println(s3 == s5); //false
System.out.println(s3 == s6); //false
System.out.println(s3 == s7); //false
System.out.println(s5 == s6); //false
System.out.println(s5 == s7); //false

String s8 = s5.intern() //返回的是字符串常量池中字面量的地址
System.out.println(s3 == s8); //true
}
}
1
2
3
4
5
6
7
String s8 = s1.concat(s2);
String s9 = "hello".concat("world");
String s10 = s1.concat("world");

System.out.println(s8 == s9); //false
System.out.println(s8 == s10);//false
System.out.println(s9 == s10);//false

构造器

public String(), 初始化新创建的String对象,以使其表示空字符序列

public String(String original) 初始化一个新创建的String对象,使其表示一个与参数相同的字符序列

public String(char[] value):通过当前参数中的字符数组来构造新的String

public String(byte[] bytes):通过使用平台的默认字符集解码当前参数中的字节数组来构造新String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
* utf8中,一个汉字3字节,一个字母1字节
* gbk中,一个汉字2字节,一个字母1字节
* 编码:String -->字节或字节数组
* 解码: 字节或字节数组 ---> String
* */

// String与char[]之间的转换
@Test
public void test5(){
String str = "hello";
// String-->char[]:调用String的toCharArray();
char[] arr = str.toCharArray();
for(int i =0;i<arr.length;i++){
System.out.println(arr[i]);
}
//char[] -->String:调用String的构造器
String str1 = new String(arr);
System.out.println(str1);
}


// String与byte[]之间的转换
@Test
public void test6(){
String str = new String("hello");
//String -->byte[]:调用String的getBytes()
byte[] arr = str.getBytes();//使用默认字符集utf-8
for(int i =0;i<arr.length;i++){
System.out.println(arr[i]);
}

//byte[] --> String
String str1 = new String(arr);
System.out.println(str1);

}

String、StringBuffer、StringBuilder

  • String:不可变字符序列.底层使用Byte[]
  • StringBuffer:可变字符序列;JDK1.0,线程安全,效率低。底层使用char[]
  • StringBuilder:可变的字符序列; JDK5.0,线程不安全,效率高,底层使用char[]

源码分析:

String s1 = new String();//Char[] value =new char[0];

String s2 = new String(“abc”);// char[] value = new char[]{‘a’,’b’, ‘c’};

StringBuilder

内部属性:

​ char[] value //存储字符序列

​ int count; //实际存储的字符个数

StringBuilder sBuffer1 = new StringBuilder(); //char[] value = new char[16];

StringBuilder sBuffer1 = new StringBuilder(“abc”);//char[] value = new char[16+”abc”.length];

sBuffer1.append(‘ac’)

…不断的添加,一旦count要超过value.length时,就需要扩容:默认扩容为原有容量的2倍+2;并将原有Value数组中的元素复制到新的数组中。

启示

  • 如果开发中需要频繁的针对于字符串的增删改,建议使用StringBuffer或StringBuilder替换String因为使用String效率低

  • 如果不涉及线程安全,则使用StringBuilder

  • 如果大体确定操作的字符个数,建议使用带int capacity参数的构造器避免底层多次扩容。

常用方法

增:

append

删:

delete(inrt start, int end)

deleteCharAt(int index)

改:

replace(int start, int end, String str)

setCharAt(int index, char c)

查:

charAt(int index)

插:

insert(int index,xx)

长度:

length()

三者效率对比:

Stringbuilder> StringBuffer>String

Date类

Jdk8之前:

System类的currentTimesMillis()

  • 获取当前时间对应的毫秒数,long类型,时间戳

  • 当前时间与1970年1月1日0时0分0秒之间的毫秒数

  • 常用来计算时间差

SimpleDateFormat

1
2
3
4
5
6
7
8
9
10
11
public void test2() throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//格式化:日期--》字符串
Date date1 = new Date();
String strDate = sdf.format(date1);
System.out.println(strDate); //2023-10-09 23:12:19

//解析:字符串--》日期
Date date2 = sdf.parse("2023-10-09 23:12:11");
System.out.println(date2);//Mon Oct 09 23:12:11 CEST 2023
}

Jdk8之后:

LocalDate\LocalTime\LocalDateTime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    @Test
public void test4(){
//获取当前时间
LocalDate localdate = LocalDate.now();
LocalTime localTime = LocalTime.now();
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localdate); //2023-10-09
System.out.println(localTime); //23:45:05.662967400
System.out.println(localDateTime); //2023-10-09T23:45:05.662967400

//of():获取指定时间
LocalDate localDate1 = LocalDate.of(2021, 5, 23);
LocalDateTime localDateTime1 = LocalDateTime.of(2022,12,23,23,34,45);
System.out.println(localDate1);
System.out.println(localDateTime1);
}
}

常用方法:

getxxxx()/withxxx()/plusxxx()/minusXXX()

Instant 类似于Date

时间线上的一个瞬时点。可能被用来记录应用程序中的事件时间戳

1
2
3
4
5
6
public void test5(){
Instant instant = Instant.now();
System.out.println(instant); //当前时间
long milliTime = instant.toEpochMilli();
System.out.println(milliTime);//当前时间至1970年的毫秒数
}

DateTimeFormatter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test6(){
//自定义格式
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");

//格式化:时间、日期-》字符串
LocalDateTime localDateTime = LocalDateTime.now();
String strDateTime = dateTimeFormatter.format(localDateTime);
System.out.println(strDateTime); //2023/10/10 12:08:02

//解析:字符串 -》日期、时间
TemporalAccessor temporalAccessor = dateTimeFormatter.parse("2023/11/25 15:29:23");
LocalDateTime localDateTime1 = LocalDateTime.from(temporalAccessor);
System.out.println(localDateTime1);

}

比较器

两种方法:自然排序、定制排序

实现comparable接口-自然排序

实现步骤:

  1. 具体的类A实现Comparable接口
  2. 重写Comparable接口中的compareTo(Object obj)方法,在此方法中指明比较类A的对象的大小的标准
  3. 创建类A多个实例,进行大小的比较和排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
    @Test
public void test2(){
Product[] arr = new Product[6];
arr[0] =new Product("aaa",5555);
arr[1] =new Product("bbb",1111);
arr[2] =new Product("ccc",3333);
arr[3] =new Product("ddd",8888);
arr[4] =new Product("eee",1111);
arr[5] =new Product("fff",2222);
Arrays.sort(arr);
for(int i =0; i<arr.length;i++){
System.out.println(arr[i]);
}
}

}

class Product implements java.lang.Comparable {
private String name;
private double price;

public Product(String name, double price) {
this.name = name;
this.price = price;
}

@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}

/*
*当前的类需要实现Comparable中的抽象方法
* 在此方法中,指明如何判断当前类的大小。
*如果返回值是正,则当前对象大
* 如果为0,则一样大
*比较标准:价格(大到小),名字排序从小到大
*/
@Override
public int compareTo(Object o) {
if(o == this){
return 0;
}
if(o instanceof Product){
Product p = (Product) o;
int value = Double.compare(this.price, p.price);
if(value != 0){
return -value;
}

return this.name.compareTo(p.name);
}
throw new RuntimeException("类型不匹配");
}
}

Comparator-定制排序

实现步骤:

  1. 创建一个实习了Comparator接口的实现类
  2. 实现类要求重写Comparator接口中的抽象方法compare(Object o1, Object o2)在此方法中指明要比较大小对象的大小关系(比如,String类、Product类)
  3. 创建此实现类A的对象,并将此对象传入到相关方法的参数位置即可(比如说:Arrays.sort(…,类A的实例)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ComparatorTest {
@Test
public void test1(){
Product[] arr = new Product[6];
arr[0] =new Product("aaa",5555);
arr[1] =new Product("bbb",1111);
arr[2] =new Product("ccc",3333);
arr[3] =new Product("ddd",8888);
arr[4] =new Product("eee",1111);
arr[5] =new Product("fff",2222);
//创建一个实现Comparator接口的实现类的对象
Comparator comparator = new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof Product && o2 instanceof Product){
Product p1 = (Product) o1;
Product p2 = (Product) o2;
return Double.compare(p1.getPrice(), p2.getPrice());
}
throw new RuntimeException("类型不匹配");
}
};
Arrays.sort(arr,comparator);

}
}

对比两种方法:

角度1:

  • 自然排序:单一的,唯一的
  • 定制排序:灵活的,多样的

角度2:

  • 自然排序:一劳永逸
  • 定制排序:临时的

角度3:

  • 自然排序:对应接口是Comparable,对应的抽象方法compareTo(Object obj)
  • 定制排序:对应接口是Comparator,对应的抽象方法compare(Object obj1, Object obj2)

Other

System类

Runtime类

对应着java进程的内存使用的运行时环境,是单例

Math类

凡是与数学运算相关的操作

BigInteger

商业计算值,要求数字精度比较高

Random类

常见问题:

两种创建String对象的方式有什么不同?

String的+怎么实现?

常量+常量:略

变量+常量、变量+变量:创建一个StringBuilder的实例,通过append()添加字符串,最后调用toString()返回一个字符串。

Java中String是不是final?

String为什么不可变,在内存中具体形态

规定不可变

String:提供字符串常量池

String可以在switch中使用

可以.从Jdk7开始使用

String中有哪些方法?

subString()底层做了什么?

底层是new的方式返回一个subStr

操作字符串的类有哪些?有什么区别?

String线程安全问题

线程不安全的

StringBuilder和StringBuffer的线程安全

Comparable和Comparator的区别和使用场景

多线程

java.lang.Thread类代表线程,所有线程对象都必须是Thread类或其子类的对象

线程创建并启动步骤

方式一:继承Thread类

  1. 创建一个继承于Thread类的子类

  2. 重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中

  3. 创建当前Thread的子类的对象

  4. 通过对象调用start():1.启动线程 2.调用当前线程的run()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class EvenNumberTest {
public static void main(String[] args) {
//创建当前Thread的子类的对象
PrintNumber t1 = new PrintNumber();
//调用对象启动start
t1.start();

//再创建一个线程
PrintNumber t2 = new PrintNumber();
t2.start();
//main()所在线程执行操作
for(int i =1; i<=10000;i++){
if(i % 2 ==0){
System.out.println(Thread.currentThread().getName()+":"+i+"****");
}
}
}
}

//创建一个继承于Thread的子类
class PrintNumber extends Thread{
//重写Thread类的run()
@Override
public void run() {
for(int i =1;i<=1000;i++){
if(i%2 ==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}

方法二:实现Runnable接口

  1. 创建一个实现Runnable接口的类
  2. 实现接口的run() –>将此线程要执行的操作,声明在此方法体中
  3. 创建当前实现类的对象
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
  5. Thread类的实例调用start()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class EvenNumberTest {
public static void main(String[] args) {
//创建当前实现类的对象
EvenNumberPrint p = new EvenNumberPrint();
//将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
Thread t1 = new Thread(p);
//Thread类的实例调用start()
t1.start();

//再创建一个线程
Thread t2 = new Thread(p);
t2.start();

//main方法对应的主线程
for(int i=1; i<=100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}


//创建一个实现Runnable接口的类
class EvenNumberPrint implements Runnable{
//实现接口的run() -->将此线程要执行的操作,声明在此方法体中
@Override
public void run() {
for(int i=1; i<=100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}

两种方式对比

共同点

​ 1.创建线程的对象,都是Thread类或其子类的实例

​ 2.启动线程都使用的是Thread类中的start()

不同点

​ 一个是类的继承,一个是接口的实现

建议使用实现Runnable接口的方式。

Runnable好处:1.避免单继承的局限性 2.更适合处理有贡献数据的问题 3.实现代码与数据的分离

二者联系:public class Thread implements Runnable

线程常用结构

线程的构造器

  • public Thread(): 分配一个新的线程对象

  • public Thread(String name):分配一个指定名字的新的线程对象

  • public Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法

  • public Thread(Runnable target, String name):分配一个带有指定目标新的线程对象并指定名字

线程中常用方法

  • start():1.启动线程 2.调用线程的run()
  • run():将线程要执行的操作,声明在run()中
  • currentThread():获取当前执行代码对应的线程
  • getName():获取线程名
  • setName():设置线程名
  • sleep(long millis):静态方法,调用时,可以使得代码所在当前线程睡眠指定的毫秒数
  • yield():静态方法,一旦执行此方法,就释放cpu的执行权
  • join(): 在线程a中通过线程b调用join(),意味着线程a进入阻塞状态,直到线程b执行结束,线程a才结束阻塞状态,继续执行
  • isAlive():判断当前线程是否存活

线程优先级

(java是抢占式调度)

Thread类的三个优先级常量

MAX_PRIORITY(10):最高优先级

MIN_PRIORITY(1):最低优先级

NORM_PRIORITY(5):普通优先级,默认情况main线程具有普通优先级

getPriority():获取线程的优先级

setPriority():设置线程的优先级,范围[1,10],大数值就有大概率

线程生命周期

JDK1.5之前:5种状态

LmeC3.png

JDK5.0之后:6种状态

Lmgm9.png

线程安全机制

当多个线程访问同一资源(同一个变量、同一个文件、同一条记录)的时候,若多个线程只有读操作,那么不会发生线程安全问题。但是如果多个线程对资源有读和写操作,就容易出现线程安全问题。

方法一:同步代码块

synchronized(同步监视器){

​ //需要被同步的代码

}

说明:

  • 需要被同步的代码,即为操作共享数据的代码
  • 共享数据,即多个线程都需要操作的数据
  • 需要被同步的代码,在被synchronized包裹以后,就是的一个线程在操作这些代码过程中。其他线程必须等待。
  • 同步监视器,俗称锁。哪个线程获取了锁,哪个线程就能执行需要被同步的代码
  • 同步监视器,可以使用任何一个类的对象。但是,多个线程必须公用同一个同步监视器。

注意:在实现Runnable接口的方式中,同步监视器可以考虑使用:this

​ 在继承Thread类的方式中,同步监视器要慎用this,可以考虑使用当前类.class

继承Thread类中的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class WindowTest {
public static void main(String[] args) {

SaleTicket w1 = new SaleTicket("窗口1");
SaleTicket w2 = new SaleTicket("窗口2");
SaleTicket w3 = new SaleTicket("窗口3");
w1.start();
w2.start();
w3.start();
}
}

class SaleTicket extends Thread{
static int ticket = 1000;
static Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (SaleTicket.class) {//this:此时表示w1,w2,w3不饿能保证唯一性
//obj:加上static,可以保证唯一性
if (ticket > 0) {

try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为" + ticket);
ticket--;

} else {
break;
}

}
}
}

public SaleTicket(String name) {
super(name);
}
}

实现Runnable接口的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class WindowTest {
public static void main(String[] args) {
SaleTicket s = new SaleTicket();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
Thread t3 = new Thread(s);

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}
}

class SaleTicket implements Runnable{
int ticket = 1000;
@Override
public void run() {
while(true){
synchronized (this) {
if (ticket > 0) {

try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为" + ticket);
ticket--;

} else {
break;
}

}
}
}
}

方式二:同步方法

说明:

  • 如果操作共享数据的代码完整声明在了一个方法中,那么我就可以将此方法声明为同步方法即可。

  • 非静态的同步方法,默认同步监视器是this

  • 静态同步方法,默认监视器是当前类本身。

继承Thread类中的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class WindowTest {
public static void main(String[] args) {

SaleTicket w1 = new SaleTicket("窗口1");
SaleTicket w2 = new SaleTicket("窗口2");
SaleTicket w3 = new SaleTicket("窗口3");
w1.start();
w2.start();
w3.start();
}
}

class SaleTicket extends Thread{
static int ticket = 1000;
static boolean isFlag = true;

@Override
public void run() {
while(isFlag){
show();

}
}

public static synchronized void show(){
if (ticket > 0) {

try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为" + ticket);
ticket--;

} else {
isFlag = false;
}



}
public SaleTicket(String name) {
super(name);
}
}

实现Runnable接口的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class WindowTest {
public static void main(String[] args) {
SaleTicket s = new SaleTicket();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
Thread t3 = new Thread(s);

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}
}

class SaleTicket implements Runnable{
int ticket = 1000;
static boolean isFlag = true;
@Override
public void run() {
while(isFlag){
show();
}
}

public synchronized void show(){ //此时的同步监视器是:this,此问题中还是唯一的
if (ticket > 0) {

try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为" + ticket);
ticket--;
}else {
isFlag=false;
}
}
}

synchronized优缺点

好处:解决了线程问题

弊端:在操作共享数据时,多线程其实是串行执行的,意味着性能低。

单例模式-懒汉式(线程安全解决版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class BankTest {
static Bank b1 = null;
static Bank b2 = null;

public static volatile void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
b1 = Bank.getInstance();
}
};

Thread t2 = new Thread(){
@Override
public void run() {
b2 = Bank.getInstance();
}
};

t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(b1);
System.out.println(b2);
System.out.println(b1==b2);
}
}

class Bank {
private Bank() {
}

private static volatile Bank instance = null;

public static synchronized Bank getInstance() {
try {
if (instance == null) {
Thread.sleep(1000);
instance = new Bank();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return instance;
}

//第二种方式(效率更高)
public static Bank getInstance(){
if(instance ==null){
synchronized(Bank.class){
if(instance==null){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
instance = new Bank();
}
}
}
return instance;
}
}


除了使用synchronized同步机制处理线程安全问题之外,还可以使用jdk5.0提供的锁方式

步骤

  1. 创建Lock的实例,需要确保多个线程公用同一个Lock实例!需要考虑将此对象声明为static final
  2. 执行lock()方法,锁定对共享资源的调用
  3. Unlock()的调用,释放对共享资源的锁定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class WindowTest {
public static void main(String[] args) {

SaleTicket w1 = new SaleTicket("窗口1");
SaleTicket w2 = new SaleTicket("窗口2");
SaleTicket w3 = new SaleTicket("窗口3");
w1.start();
w2.start();
w3.start();
}
}

class SaleTicket extends Thread{
static int ticket = 1000;
static boolean isFlag = true;
//1.创建Lock的实例,需要确保多个线程公用一个Lock实例,需要考虑将此对象声明为static finally
private static final ReentrantLock lock =new ReentrantLock();
@Override
public void run() {
while(isFlag){
try {
//2. 执行lock方法,锁定对共享资源的调用
lock.lock();
show();
}finally {
lock.unlock();
}
}
}

public void show(){
if (ticket > 0) {

try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号为" + ticket);
ticket--;
} else {
isFlag = false;
}
}
public SaleTicket(String name) {
super(name);
}
}

Lock与synchronized的区别

synchronized不管是同步代码块还是同步方法,都需要在结束一对{},释放对同步监视器的调用。

Lock是通过两个方法控制需要被同步的代码,更灵活一些。

Lock作为接口,提供了多种实现类,适合更多更复杂,效率更高。

通信

线程间通信的理解

当我们’需要多个线程’来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些通信可以协调它们的工作,以此实现多线程共同操作一份数据。

涉及到三个方法的使用

wait():线程一旦执行此方法,就进入等待状态,同时会释放对同步监视器的调用。

notify():一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。(如果被wait()的多个线程优先级相同就随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行。

notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

注意

  • 此三个方法的使用,必须是在同步代码块或同步方法中。
  • 此三个方法的调用者,必须是同步监视器。否则会报IllegalMontiorStateException异常
  • 此三个方法声明在Object类中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class PrintNumberTest {
public static void main(String[] args) {
PrintNumber p = new PrintNumber();
Thread t1 = new Thread(p,"线程1");
Thread t2 = new Thread(p,"线程2");
t1.start();
t2.start();
}
}

class PrintNumber implements Runnable{
private int number =1;

@Override
public void run() {
while(true){
synchronized (this) {
notify();
if(number<=100){

try {
Thread.sleep(10);
} catch (Exception e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+":"+number);
number++;

try {
wait(); //线程一旦执行此方法,就进入等待状态。同事,会释放对同步监视器的调用。
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
break;
}
}
}
}
}

wait()和sleep()的区别

相同点:一旦执行,当前线程都会进入阻塞状态

不同点

声明位置不同:wait():声明在Object类中

​ sleep()声明在Thread类中,静态的

使用场景不同:wait():只能使用在同步代码块或同步方法中

​ sleep():可以在任何需要使用的场景

使用在同步代码块中:wait()一旦执行会释放同步监视器

​ sleep()一旦执行,不会释放

结束阻塞的方式:wait()到达指定时间自动结束阻塞或通过被notify唤醒,结束阻塞

sleep():到达指定时间自动结束阻塞。

消费者与生产者练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public class ProducerConsumerTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer pro1 = new Producer(clerk);
Consumer con1 = new Consumer(clerk);
pro1.setName("生产者1");
con1.setName("消费者");
pro1.start();
con1.start();

}
}

class Clerk{
private int ProductNum = 0;

//增加产品数量方法
public synchronized void addProduct(){
if(ProductNum >= 20){
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
ProductNum++;
System.out.println(Thread.currentThread().getName()+"生产了第"+ProductNum+"个产品");
notify();
}

//减少产品数量方法
public synchronized void minusProduct(){
if(ProductNum<=0){
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
ProductNum--;
System.out.println(Thread.currentThread().getName()+"消费了第"+ProductNum+"个产品");
notify();
}

}
}

class Producer extends Thread{
private Clerk clerk;
public Producer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
while (true) {
System.out.println("生产者生产产品");
try{
Thread.sleep(20);
}catch (InterruptedException e){
e.printStackTrace();
}
clerk.addProduct();
}
}
}

class Consumer extends Thread{
private Clerk clerk;
public Consumer(Clerk clerk){
this.clerk = clerk;
}

@Override
public void run() {
while (true) {
System.out.println("生产者生产产品");
try{
Thread.sleep(50);
}catch (InterruptedException e){
e.printStackTrace();
}
clerk.minusProduct();
}
}
}

线程创建方式三-Callable()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//1.创建一个实现Callable的实现类
class NumThread implements Callable {
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for(int i = 1; i<=100; i++){
if(i%2 ==0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}

public class CallableTest{
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();

//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);

//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
Thread t1 = new Thread(futureTask);
t1.start();

try {
//6.获取Callable中call方法的返回值
//get()返回值即为FutreTask构造器参数Callable实现类重写的call()的返回值
Object sum = futureTask.get();//(自带一个阻塞)
System.out.println("总和为" + sum);
} catch (Exception e) {
e.printStackTrace();

}
}
}

与Runnable方式的对比

  • call()可以有返回值,更灵活
  • call()可以使用throws方式处理异常,更灵活
  • Callable使用了泛型参数。可以指明具体的call()的返回值类型,更灵活

缺点:

如果在主线程中需要获取分线程call()的返回值,则此时的主线程是阻塞状态的

线程创建方式四-使用线程池

此方式的好处:

  • 提高了程序执行的效率(因为线程已经提前创建好了)
  • 提高了资源的复用率(因为执行完的线程并未销毁,而是可以继续执行其他的任务)
  • 可以设置相关的参数,对线程池的线程的使用进行管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class ThreadPool {
public static void main(String[] args) {
//1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;

//设置线程池的属性
service1.setMaximumPoolSize(50);//设置线程池中线程数的上限

//2.执行指定的线程的操作-需要提供实现Runnable接口或Callable接口1实现类的对象
service.execute(new NumberThread());
service.execute(new NumberThread1());

//3.关闭连接池
service.shutdown();
}
}

class NumberThread implements Runnable{
@Override
public void run() {
for(int i =0;i<=100;i++){
if(i%2 ==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}

class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i=0;i<=100;i++){
if(i%2!=0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}

常见问题

1.线程和进程相关定义

2.多线程使用场景

  • 手机app应用图片的下载
  • 迅雷的下载
  • Tomcat服务器上web应用,多个客户端发起请求,Tomcat针对多个请求开辟多个线程

3.如何实现多线程

4.Thread类中的Start()和run()的区别?

start():1.开启线程 2.调用线程的run()

5.sleep()和yield()区别?

sleep():一旦调用,就进入阻塞状态(或TiMED_WAITING状态)

yield():释放cpu的执行权,处在runnable的状态

6.线程生命周期

ex:线程有哪些状态,如何让线程进入阻塞

ex线程有几个状态,就绪和阻塞有什么不同

7.线程优先级怎么定义

8.stop()和suspend()方法为何不推荐使用

stop():一旦执行,线程就结束了。导致run()有未执行结束的代码。stop()会导致释放同步监视器,导致线程安全

suspend():与resume()搭配使用,导致死锁

9.线程安全的理解,线程安全如何造成的

10.多线程公用一个数据变量需要注意什么

11.多线程保证线程安全一般有几种方式

同步机制

Lock接口

12.用什么关键字修饰同步方法?

13.synchronized加在静态方法和普通方法的区别

同步监视器不同。静态:类本身 非静态:this

14.当一个线程进入一个对象的一个synchronized方法后,其他线程是否可以进入此对象的其他方法?

需要看其他方法是否使用synchronized修饰,同步监视器的this是否是同一个。

只有当使用了synchronized且this是同一个的情况下,就不能访问

15.线程同步与阻塞的关系?同步一定阻塞吗?阻塞一定同步吗?

同步一定阻塞。 阻塞不一定同步

16.wait和notify方法为什么一定要在同步块中调用?

调用者必须是同步监视器。

17.生产者、消费者问题

18.wait和sleep的区别

19.手写单例模式。

异常定义

指的是程序在执行过程中,出现的非正常情况,如果补处理最终会导致JVM的非正常停止。

异常的抛出机制

java中把不同的异常用不同的类表示,一旦发现某种异常,就创建该异常类型的对象,并throw。然后程序员可以catch到这个异常并处理;如果没有catch这个异常对象,那么这个异常对象将会导致程序的终止

如何对待异常

  • 一遇到错误就终止程序的运行

  • 编写程序时,充分的预防和避免。实在无法避免的,编写相应的代码进行异常的检测、以及‘异常的处理‘,保证代码的健壮性

异常的体系结构

Throwable类

是java执行过程总发生异常事件对应的类的根父类

Error和Exception

  • Error:java虚拟机无法解决的严重问题。如JVM系统内部错误、资源耗尽等严重情况。一般不编写针对性代码处理

    • StackOverflowError, OutOfMemoryError
  • Exception:其他因编程错误或偶然的外在因素导致的一般性问题,需要使用针对性的代码进行处理,使程序继续运行。否则一旦发生异常,程序也会挂掉。如空指针访问、试图读取不在的文件、网络连接中断。

    • 编译时异常:执行javac.exe命令时出现异常
      • ClassNotFoundException
      • FileNotFoundException
      • IoException
    • 运行时异常:执行java.exe命令时出现异常
      • ArrayIndexOutOfBoundsException
      • NullPointerException
      • ClassCastException
      • NumberFomatException
      • InpuitMismatchException
      • ArithmeticException

编译时异常和运行时异常

  • 编译时异常(checked异常):在代码编译阶段,编译器就能明确警示当前代码可能发生xx异常,并且明确督促程序员提前编写处理它的代码。 如果程序员没有编写对应的处理代码,则编译器就会直接判定编译失败,从而不能生产字节码文件。这类异常的发生不是由程序员的代码引起的或者不是靠加简单判断可以避免的。如FileNotFoundException

  • 运行时异常(runtime异常):在代码编译阶段,编译器完全不做任何检查,无论该异常都不给任何提示。只有等代码运行起来并确实发生了xx异常,它才能被发现。通常,这类异常是由程序员的代码编写不当引起的。RuntimeException

Java异常处理的方式

方式一 try-catch-finally

  1. “抛”:程序执行过程中,一旦出现异常,就会在异常的代码处,生产对应异常类的对象,并将对象抛出,一旦抛出,此程序就不执行其后的代码了。
  2. “抓”:针对于1中抛出的异常,进行捕获处理。一旦将异常进行了处理,代码就可以正常处理

基本结构

1
2
3
4
5
6
7
8
9
10
11
12
try{
...//可能产生异常的代码
}
catch(异常类型1){
...//产生异常1类型时的处理方式
}
catch(异常类型2){
...//产生异常2类型时的处理方式
}
finally{
...//无论是否发生异常,都无条件执行的语句
}

使用细节:

  • 如果声明了多个catch结构,不同的异常类型若不存在父子关系的情况下,声明位置无所谓。如果多个异常类型满足父子关系的,则必须将子类声明在父类结构的上面。

  • try中声明的变量,出了try结果后,就不可以进行调用了

  • try catch结构是可以嵌套的

Catch中处理异常的方式

  1. 自己编写输出的语句

  2. printStackTrace():打印异常详细信息。(推荐)

  3. getMessage():获取发生异常的原因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test1(){
try {
String str = "123";
str = "abc";
int i = Integer.parseInt(str);
System.out.println(i);
}catch (NumberFormatException e){
e.printStackTrace();
System.out.println(e.getMessage());

}
System.out.println("程序结束");
}

开发体会:

  • 运行时异常:

    ​ 开发中,通常就不进行显示的处理。一旦在程序执行中,出现了异常,就根据异常的提升信息修改代码。

  • 编译时异常:

    ​ 一定要处理,否则编译不过。

finally的理解

  • 将一定要执行的代码声明在finally结构中

  • 无论try或catch中是否存在仍未被处理的异常,无论try中或catch中是否存在return语句等,finally中声明的语句一定会执行

  • finally语句和catch语句是可选的,但finally不能单独使用

什么代码一定要放入finally

输入流、输出流、数据库连接、Socket连接等资源,在使用完以后,必须是显式的进行关闭操作,否则GC不会自动的回收这些资源,进而导致内存的泄露

方式二:throws

格式: 在方法的声明处,使用throws 异常1,异常2,…

是否真正处理了异常:

  • 从编译是否能通过的角度看,看成给出了异常万一要是出现时候的解决方法。此方案就是,继续向上抛出
  • 但是此throws方式,仅是将可能出现的异常抛给方法的调用者。此调用者仍然需要考虑如何处理相关异常,从这个角度看,throws的方式不算真正意义上处理了异常。

方法重写时的要求(针对于编译时异常):

子类重写方法抛出的异常类型要么与父类的方法抛出的异常相同或者是该异常类型的子类

开发中如何选择异常处理方法

  • 涉及资源的调用(流、数据库连接、网络连接),try-catch-finally,从而保证内存不泄露
  • 父类被重写的方法没有throws异常类型,子类重写方法出现异常只能使用try-catch-finally
  • 开发中方法a依次调用b,c,d等方法,方法b,c,d之间是递进关系。此时如果b,c,d中有异常,我们通常选择使用throws,而方法a中通常使用try-cat-finally

手动throw异常对象

在实际开发中,如果出现不满足具体场景的代码问题,我们就有必要抛出一个指定类型的异常对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class StudentTest {
public static void main(String[] args) {
Student s1 = new Student();
s1.regist(-10);
System.out.println(s1);
}
}

class Student{
int id;

public void regist(int id) {
if (id > 0) {
this.id = id;
}else{
throw new RuntimeException("输入ID非法");
}
}

@Override
public String toString() {
return "Student{" +
"id=" + id +
'}';
}
}

自动抛和手动抛异常的理解

  • 自动抛:程序执行过程中,一旦出现异常,就会在异常的代码处,自动生产对应异常类的对象,并将对象抛出
  • 手动抛:程序执行过程中,不满足指定条件的情况下,我们主动使用throw+异常类的对象方式抛出异常对象。

抓:

  • 狭义理解:try-catch的方式捕获异常,并处理
  • 广义理解:抓可以理解为“处理“。则此时对应着1.try-catch-finally 2.throws

自定义异常:

定义方式

  1. 通常继承于RuntimeException、Exception

  2. 通常提供几个重载的构造器

  3. 提供一个全局常量,声明为static final long serialVersionUID

使用方式

  • 在具体代码中,满足指定条件的情况下,需要收到的使用“throw+自定义异常类的对象”方法,将异常对象抛出。
  • 如果自定义异常类是非运行时异常,则必须考虑如何处理此异常类的对象(1.try-cat-finally 2.throws)

自定义异常的目的

通过异常的名称就能直接判断此异常出现的原因。尽然如此,我们就有必要在实际开发场景中,不满足我们指定条件时,知名我们自己特有的异常类,通过异常类的名称,就能判断出具体出现的问题

常见问题:

1.throw 和throws的区别

形:

throw:使用在方法内部,throw异常类的对象

throws:使用在方法的声明处,”throws 异常类1, 异常类2”

作用:

一个抛,一个是抓

2.java的异常体系结构,异常顶级接口是什么,exception下有哪些类

3.java异常处理机制

两种处理方案,try-catch-finally; throws

4.运行异常与一般异常区别

运行时异常:RuntimeException

  • 编译可以通过,运行时可能抛出。出现概率高一些;一般针对于运行时异常,都不处理

一般异常:Exception

  • 编译不能通过,要求必须在编译之前,考虑异常的处理。不处理编译不通过。

5.常见的异常

6.final、finally、finalize的区别

7.不适用try-catch,程序出现异常会如何?

对当前方法来讲,如果不适用try-catch,则在出现异常对象以后会抛出此对象。如果没有处理方案,就会终止程序的执行

8.try,catch捕获的是什么异常?

Exception。非error

9.如果执行finally代码块之前返回了结果或者Jvm退出了,此时finally代码块还会执行吗?

一定会执行。除了system.exit(0)

10.try语句中有return语句,最后写finally语句,finally语句中的code会不会执行?何时执行?如果执行是在return前还是后

会。先执行finally代码在return出结果

枚举类

枚举类本质上也是一种类,只不过这个类的对象是有限的、固定的几个,不能让用户随意创建。

  • 星期

  • 性别

  • 月份

  • 积极

  • 三原色

  • 支付方式

  • 线程状态

  • 就职状态

  • 订单状态

开发建议:如果针对于某个类,其实例是确定个数的。则推荐将此类声明为枚举类

若枚举只有一个对象,则可以作为一种单例模式的实现方式

实现方法:

java支持enum关键字类快速定义枚举类型

JDK5.0之前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class SeasonTest {
public static void main(String[] args) {
System.out.println(Season.SPRING);
}
}

//jdk5.0
class Season{
//2.声明当前类的对象的实例变量
private final String seasonName;
private final String seasonDesc;

//1.私有化构造器
private Season(String seasonName,String seasonDesc){
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}


//3.提供实例变量的get
public String getSeasonName() {
return seasonName;
}

public String getSeasonDesc() {
return seasonDesc;
}

//4.创建当前类的实例
public static final Season SPRING = new Season("Spring", "Flowers");
public static final Season SUMMER = new Season("Summer", "Sunshine");
public static final Season AUTUMN = new Season("Autumn", "Wind");
public static final Season WINTER = new Season("Winter", "Snow");

}
}

5.0之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
enum Season1{
//创建当前类的实例
SPRING("Spring", "Flowers"),
SUMMER("Summer", "Sunshine"),
AUTUMN("Autumn", "Wind"),
WINTER("Winter", "Snow");

//2.声明当前类的对象的实例变量
private final String seasonName;
private final String seasonDesc;

//1.私有化构造器
private Season1(String seasonName,String seasonDesc){
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}


//3.提供实例变量的get
public String getSeasonName() {
return seasonName;
}

public String getSeasonDesc() {
return seasonDesc;
}

}

Enum常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//1. toString() 返回的是常量名,可以重写该方法
System.out.println(Season1.SPRING);
//2.name():得到当前枚举常量的名称,toString()优先
System.out.println(Season1.SPRING.name());
//3.values() 返回枚举类型的对象数组。便于遍历所有枚举值是一个静态方法
Season1[] values = Season1.values();
for (int i= 0; i<values.length;i++){
System.out.println(values[i]);
}
//4.valueOf(String objName):返回当前枚举类型中名称为objName的枚举类对象
//如果枚举类中不存objName则报错
String objName = "WINT1";
Season1 season1 = Season1.valueOf(objName);
System.out.println(season1);
//5.oridnal():返回当前枚举常量的次序号
System.out.println(Season1.AUTUMN.ordinal());

枚举类实现接口的操作:

  • Case1:枚举类实现接口,在枚举类中重写接口的抽象方法。当通过不同的枚举类对象调用此方法时,执行的是同一个方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    enum Season1 implements Info{
    SPRING("Spring", "Flowers"),
    SUMMER("Summer", "Sunshine"),
    AUTUMN("Autumn", "Wind"),
    WINTER("Winter", "Snow");

    //2.声明当前类的对象的实例变量
    private final String seasonName;
    private final String seasonDesc;

    //1.私有化构造器
    private Season1(String seasonName,String seasonDesc){
    this.seasonName = seasonName;
    this.seasonDesc = seasonDesc;
    }


    //3.提供实例变量的get
    public String getSeasonName() {
    return seasonName;
    }

    public String getSeasonDesc() {
    return seasonDesc;
    }

    @Override
    public void show() {

    }
    }
  • Case2:让枚举类的每一个对象来重写接口的抽象方法。这样枚举的每一个对象都可以有不同的重写方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    interface Info{
    void show();
    }
    enum Season1 implements Info{
    SPRING("Spring", "Flowers"){
    @Override
    public void show() {

    }
    },
    SUMMER("Summer", "Sunshine"){
    @Override
    public void show() {

    }
    },
    AUTUMN("Autumn", "Wind"){
    @Override
    public void show() {

    }
    },
    WINTER("Winter", "Snow"){
    @Override
    public void show() {

    }
    };

    //2.声明当前类的对象的实例变量
    private final String seasonName;
    private final String seasonDesc;

    //1.私有化构造器
    private Season1(String seasonName,String seasonDesc){
    this.seasonName = seasonName;
    this.seasonDesc = seasonDesc;
    }


    //3.提供实例变量的get
    public String getSeasonName() {
    return seasonName;
    }

    public String getSeasonDesc() {
    return seasonDesc;
    }

    }

实现一个单例模式

1
2
3
4
5
6
7
8
enum GirlFriend{
yuanyuan(20);
private final int age;
private GirlFriend(int age){
this.age = age;
}

}

Ex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class ColorTest {
public static void main(String[] args) {
System.out.println(Color.RED);
}

}

enum Color{
RED(255,0,0,"红色"),
ORDANGE(255,128,0,"橙色"),
GREEN(0,255,0,"绿色");

private final int red;
private final int green;
private final int blue;
private final String desc;

Color(int red, int green, int blue, String desc) {
this.red = red;
this.green = green;
this.blue = blue;
this.desc = desc;
}

public int getRed() {
return red;
}

public int getGreen() {
return green;
}

public int getBlue() {
return blue;
}

public String getDesc() {
return desc;
}

@Override
public String toString() {
return name()+"("+red+","+green+","+blue+")"+desc;
}
}

注解

Annotation可以像修饰符一样被使用,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明。还可以添加一些参数值,这些信息被保存在Annotation的”name=value”对中

注解与注释区别:

注解也可以看做是一种注释。通过使用注解,程序员可以在不改变原有逻辑的情况相爱,在源文件嵌入一些补充信息。但是,注解不同于单行注释和多行注释

  • 单行注释和多行注释是给程序员看的。
  • 而注解是可以被编译器或其他程序读取的。程序还可以根据注解的不同,做出相应的处理

应用场景

  • 生成文档注释

  • 在编译时进行格式检查(JDK内置的三个基本注解)

  • 跟踪代码依耐性,实现替换配置文件功能

三个常用注解

  • @Override:限定重写父类方法,该注解只能用于方法
  • @Deprecated:用于表示所修饰的(类、方法)已过时。通常是因为所修饰的结构危险或存在更好的选择。
  • @SuppressWarnings:抑制编译器警告

自定义注解

以@SuppressWarnings为参照,进行定义极客。

单元测试

测试分类

黑盒测试:不需要写代码,给输入值,看程序是否能够输出期望值。

白盒测试:需要写代码。关于程序具体的执行流程。

利用JUnit进行测试

说明

正确编写单元测试方法,需要满足

  • 所在的类必须是Public的,非抽象的,包含唯一的无参构造器

  • @Test标记的方法本身必须是public,非抽象的,非静态的,void无返回值,()无参数的。

包装类

为了让基本数据类型具备引用数据类型变量的相关特征(封装性、继承性、多态性)

基本数据类型

Java针对于八种基本数据类型定义了相应的引用类型:包装类。

L2DmW.png

基本数据类型转包装类(装箱)

手动装箱

1
2
3
4
5
6
7
8
9
10
11
12
public class WrapperTest {
public void test1(){
int i1 = 10;
Integer ii1 = Integer.valueOf(i1);
System.out.println(ii1.toString());

Float f1 = 12.3F;
Float ff1 = Float.valueOf(f1);
System.out.println(ff1.toString());

}
}

自动装箱

1
2
3
4
5
public void test2(){
int i1 = 10;
Integer ii1 = i1;
Float f1 = 12.3F;
}

包装类转基本数据类型(拆箱)

1
2
3
4
5
6
7
8
9
10
11
12
public class WrapperTest {
@Test
public void test1(){
Integer ii1 = Integer.valueOf(i1);
int i1 = ii1.intValue();
i1 = i1+1;

Float ff1 = Float.valueOf(12.3F);
float f1 = ff1.floatValue();

}
}

自动拆箱

1
2
3
4
5
public void test(){
Integer ii2 = 15;
int i2 = ii2;

}

String与基本数据类型、包装类之间的转换

基本数据类型、包装类 转 String

1
2
3
4
5
6
7
8
9
public void test4(){
int i5 =10;
String s1 = String.valueOf(i5);
System.out.println(s1);

Integer ii5 = 10;
String s2 = String.valueOf(ii5);
System.out.println(s2);
}

String转基本数据类型

1
2
3
4
public void test6(){
String s5 = "123";
int i1 = Integer.parseInt(s5);
}

自动装箱时,若数值在缓存对象区间,两个包装类相等。若超出Integer m=128, Integer n=128,此时m不等于n

LFLm5.png

接口(interface)

1.接口: 本质就是契约、规范、标准

2.接口关键字:interface

3.接口内部结构的说明:

可以声明

  • ​ 属性:必须使用public staic final 修饰

  • ​ 方法: jdk8之前:声明抽象方法,修饰为public abstract

    ​ jdk8:声明静态方法、默认方法

    ​ jdk9:声明私有方法

不可以声明:构造器、代码块

4.接口与类的关系:实现关系

5.格式:class A extends super A implements B,C{}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface Flyable {
//全局常量
public static final int MIN_SPEED = 0;
//可以省略 public static final
int MAX_SPEED = 7900;
//可以省略 public abstract
public abstract void fly();
}
interface Attackbale{
public abstract void attack();
}

abstract class Plane implements Flyable{
}

class Bullet implements Flyable, Attackbale{
@Override
public void fly() {
System.out.println("bullet fly");
}

@Override
public void attack() {
System.out.println("Bullet attack");
}
}

常见实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class USBTest {
public static void main(String[] args) {
//1.创建接口实现类的对象
Computer c1 = new Computer();
Printer p1 = new Printer();
c1.transferData(p1);

//2.创捷接口实现类的匿名对象
c1.transferData(new Camera());

//3.创建接口匿名实现类的对象
USB usb1 = new USB(){
public void start(){
System.out.println("U盘开始工作");
}
public void stop(){
System.out.println("U盘停止工作");
}
};
c1.transferData(usb1);

//4.创建接口匿名实现类的匿名对象
c1.transferData(new USB() {
@Override
public void start() {
System.out.println("xx开始工作");
}

@Override
public void stop() {
System.out.println("xx停止工作");
}
});
}
}
----------------------------------------------------------------------------------------------

class Computer{
public void transferData(USB usb){ //多态 USB usb = new Printer();
System.out.println("设备链接成功");
usb.start();
System.out.println("数据传续细节");
usb.stop();
}
}

class Printer implements USB{
@Override
public void start() {
System.out.println("开始工作打印机");
}

@Override
public void stop() {
System.out.println("打印机停止工作");
}
}
class Camera implements USB{
@Override
public void start() {
System.out.println("照相机开始");
}
@Override
public void stop() {
System.out.println("照相机停止工作");
}
}

interface USB{
public abstract void start();
void stop();
}

6.说明

  • 类可以实现多个接口
  • 类针对于接口的多实现,一定程度上就弥补了类的单继承性和局限性
  • 类必须将实现的接口中的所有抽象方法重写,方可实例化。否则类必须得声明为抽象类
  • 重写的方法的访问级别不小于原方法

7.接口和接口关系:继承关系,且可以多继承

8.接口的多态性: 接口名 变量名 = new 实现类对象

9.区分抽象类和接口

  • 共性:都可以声明抽象方法,都不能实例化

  • 不同:抽象类一定有构造器,接口没有构造器

    ​ 类与类直接继承关系,类与接口之间是实现关系,接口与接口直接是多继承关系

10.jdk8之后引入了default方法,引用了静态和default方法

  • 接口类声明的静态方法,只能由接口
  • 接口中声明的默认方法可以被实现类继承,实现类在没有重写此方法的情况下,默认调用接口中声明的方法。
  • 类实现了两个接口,而两个接口中定义了同名同参数的默认方法。如果实现类没有重写此两个接口的方法,则会报错接口冲突
  • 子类(或实现类)继承了父类并实现了接口。父类和接口中声明了同名同参数的方法。子类(或实现类)在类没有重写的情况下,默认调用的是父类的方法。
  • 实现类继承了两个接口的两个同名方法。指定调用方法可采用”接口.super.方法()”

内部类

定义:将一个类A定义在另一个类B里面,类A为内部类,类B为外部类

为什么需要内部类:当一个事物A的内部,还有一个部分需要一个完整的结构B进行描述,而这个完整的结构B又只为外部事物A提供服务,不在其他地方单独使用,那么整个内部的完整结构B最好使用内部类

分类

  • 成员内部类:直接声明在外部类里面

    • 使用static修饰: 静态的成员内部类

    • 不使用static修饰:非静态的成员内部类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      public class PersonTest {
      public static void main(String[] args) {
      //1.创建Person的静态成员内部类的实例
      Person.Dog dog = new Person.Dog();
      dog.eat();//chien manger

      //2.创建Person的非静态成员内部类的实例
      Person p1 = new Person();
      Person.Bird bird = p1.new Bird();
      bird.eat();//Bird manger
      bird.show("YY");//age1, nameYY, this.nameTTN, person.nameTom
      bird.show1();
      }
      }

      public class Person {
      String name = "Tom";
      int age = 1;

      //静态成员内部类
      static class Dog{
      public void eat(){
      System.out.println("chien manger");
      }
      }
      //非静态成员内部类
      class Bird {
      String name="TTN";
      public void eat(){
      System.out.println("Bird manger");
      }
      public void show(String name){
      System.out.println("age"+age);
      System.out.println("name"+name);
      System.out.println("this.name"+this.name);
      System.out.println("person.name"+Person.this.name);
      }
      public void show1(){
      eat();
      this.eat();
      Person.this.eat();
      }
      }

      public void eat(){
      System.out.println("人吃饭");
      }
      //局部内部类
      public void method(){
      class InnerClass1{

      }
      }
      public Person(){
      class InnerClass1{

      }
      }
      {
      class InnerClass1{

      }
      }
      }
  • 局部内部类:声明在方法内、构造器内、代码块内的内部内

    • 匿名成员内部类

    • 非匿名成员内部类

关于成员内部类的理解

  • 从类的角度看:

    • 内部可以声明属性、方法、构造器、代码块、内部类等结构

    • 此内部类可以声明父类、可以实现接口

    • 可以使用final修饰

    • 可以使用abstract修饰

  • 从外部成员的角度来看

    • 在内部可以调用外部类的结构。比如属性、方法
    • 除了使用public、缺省权限修饰之外。还可以使用private、protected
    • 可以使用static

Ex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public class OuterClass {
public static void main(String[] args) {
//接口ex1
SubA a1 = new SubA();
a1.method();

//接口ex2
A a2 = new A() {
public void method() {
System.out.println("匿名实现类的对象");
}
};
a2.method();

//接口ex3
new A() {
public void method() {
System.out.println("匿名实现类的匿名对象");
}
}.method();


//抽象类 ex1
SubB s1 = new SubB();
s1.method1();
//抽象类 ex2
B b1 = new B() {
@Override
public void method1() {
System.out.println("匿名重写");
}
};
b1.method1();
//抽象类 ex3
new B() {
@Override
public void method1() {
System.out.println("匿名重写匿名对象");
}
}.method1();

//内部类ex6
C c = new C();
c.method2();

//内部类ex7
C c1 = new C(){
@Override
public void method2() {
System.out.println("xxix");
}
};
c1.method2();
//内部类ex8
new C(){
@Override
public void method2() {
System.out.println("haha");
}
}.method2();

}
}
------------------------------------------------------------------------------------------
interface A{
public void method();
}

class SubA implements A{
@Override
public void method() {
System.out.println("subA");
}
}

abstract class B{
public abstract void method1();
}

class SubB extends B{
@Override
public void method1() {
System.out.println("SubB");
}
}

class C{
public void method2(){
System.out.println("ccc");
}
}

常见问题

1.接口与抽象类的区别

LALij.png

2.接口是否可继承接口,抽象类是否可实现接口?抽象类是否可以继承实现类?

是;是;是;

3.接口可以有自己属性吗?

可以。必须是public static final

4.访问接口的默认方法如何使用?

使用实现类的对象进行调用。而且实现还可以重写此默认方法。

5.内部类的特点说一下

6.匿名类说一下

类属性、类方法

有时希望无论是否产生了对象或无论产生了多少对象的情况下,某些特定的数据在内存空间里只有一份。

static修饰的结构:属性、方法、代码块、内部类

静态变量

对比静态变量与实例变量:

​ 1.个数:

​ 静态变量:内存空间只有一份,被类的多个对象共享。

​ 实例变量:类的每一个实例都保存着一份实例变量

​ 2.内存位置:

​ 静态变量:jdk6之前存放在方法区。 jdk7及之后,存放在堆空间

​ 实例变量:存放在堆空间的对象实体中

​ 3.加载时机

​ 静态变量:随着类的加载而加载,由于类只会加载一次,所以静态变量也只有一份

​ 实例变量:随着对象的创建而创建。每个对象拥有一份实例变量

​ 4.调用者

​ 静态变量:可以被类和实例调用

​ 实例变量:只能通过实例来调用

​ 5.消亡时机

​ 静态变量:随着类的卸载而消亡

​ 实例变量:随着对象的消亡而消亡

静态方法

  • 随着类的加载而加载

  • 可以通过“类.静态加载”,直接调用静态方法

  • 静态方法可以调用静态的属性和方法,但不可以调用任何非静态的结构

    但非静态方法可以调用当前类中的静态结构

什么时候需要将属性声明为静态的

  • 判断当前类的多个实例是否能共享此成员变量,且此成员变量的值是相同的
  • 常将一些常量声明为静态,如Math类中的PI

什么时候需要将方法声明为静态?

  • 方法内操作的变量如果都是静态变量(而非实例变量),则建议声明为静态方法
  • 常常将工具栏的方法声明为静态方法。比如Array类、Math类

单例设计模式(Singleton)

定义:某个类只能存在一个对象实例

实现思路

如果要让虚拟机只能产生一个对象。首先必须将类的构造器的访问权限设置为private,这样就不能用new操作符在类的外部产生类的对象了,但在类的内部可以产生该类的对象,因为在类的外部开始无法得到类的对象,只能通过调用类的某个静态方法以返回类内部创建的对象,静态方法只能访问类中的静态成员变量,所以只能类内部产生的该类对象的变量也必须定义成静态的

具体实现方式

饿汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Bank{
//1.类的构造私有化
private Bank(){

}

//2.在类的内部创建当前实例
//4.此属性也必须声明为static
private static Bank instance = new Bank();

//3.使用getXXx()获取当前实例,必须声明为static
public static Bank getInstance(){
return instance;
}

}

//懒汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class GirlFriend{
//1.类的构造器私有化
private GirlFriend(){}

//2.声明当前类的实例
//4.此属性也必须声明static
private static GirlFriend instance = null;

//3.通过getXX()获取当前类的实例,如果未创建,则在方法内部创建
public static GirlFriend getInstance(){
if(instance ==null){
instance = new GirlFriend();
}
return instance;
}
}

特点对比

  • 饿汉式:立即加载,随着类的加载,当前唯一实例就创建了

  • 懒汉式:延迟加载,在需要使用时,进行创建了

优缺点

  • 饿汉式:写法简单,由于内存中较早加载,使用更方便、更快。是线程安全的。(缺点)内存中占用实际较长
  • 懒汉式:在需要时进行创建,节省内存空间。(缺点)线程不安全

Main方法

1
2
public static void main(String[] args) {
}

理解1:看作一个普通的静态方法

理解2:看作是程序的入口,格式的固定的

2.与控制台交互

方式1:使用Scanner

方式2:使用main()的形参进行传递

代码块

作用:用于初始化类或对象的信息

代码块的修饰:只能使用static进行修饰

具体使用:

1.静态代码块:

  • 随着类的加载而执行

  • 由于类的加载只有一次,进而静态代码块的执行,也只会执行一次

  • 作用:用来初始化类的信息

  • 内部可以声明变量、调用属性或方法、编写输出语句

  • 静态先执行,非静态后执行

  • 静态代码块内部只能调用静态结构

2.非静态代码块

  • 随着对象的创建而执行
  • 每创建当前类的一个实例,就会执行一次非静态代码块
  • 作用:用来初始化对象的信息
  • 内部可以声明变量、调用属性或方法、编写输出语句
  • 可以调用静态结构和非静态结构

当赋值的值,一行代码没有办法获得该值时,我们可以考虑使用代码块

final关键字

final可以用于修饰类、方法、变量

类:表示此类不能被继承

​ 比如:String,SringBuffer, StringBuilder{}

方法:表示此方法不能被重写

​ 比如 Object类中getClasse()

变量:既可以修饰成员,也可以修饰局部变量。 此时变量其实变成了常量

​ 1.final修饰成员变量:哪些位置可以给成员变量赋值?

  • 显式赋值
  • 代码块中
  • 构造器中

​ 2.final修饰局部变量,一旦赋值不能改变

  • 方法内声明的局部变量:调用局部变量前,一定需要赋值,一旦赋值,不可更改
  • 方法的形参:在调用此方法时,给形参进行赋值

final与static搭配修饰成员变量时:此成员变量称为全局常量

比如Math中的PI

经典题目

1.静态变量和实际变量区别和使用场景

2.静态方法是否可以被继承?是否可以被重写?

静态方法不能被重写

父子类两同名方法,如果不是静态就是重写, 如果都是静态,就是两个独立的方法,静态方法不存在多态性。

但是可以重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person{
public static void eat(){
sout("爸爸吃饭");
}
}

class Son extends Person{
public static void eat(){
sout("儿子吃饭");
}
}

Public class Test{
main(){
Person p1 = new Son();
p1.eat(); 输出的是爸爸吃饭

}
}

3.是否可以从一个static方法内部发出对非static方法的调用

不可以。 静态方法只能访问静态成员。

如果可以也只能通过对象来对非静态方法调用

4.被static修饰的成员(类、方法、成员变量)能否再使用private进行修饰

完全可以。除了代码块

5.main()方法的Public能不能换成private,为什么

可以改。但是改完以后不是程序入口了

6.main()方法是否可以调用非静态方法

只能通过对象来对非静态方法调用

7.类的组成和属性赋值执行顺序

8.静态代码块、普通代码块,构造方法,从类加载开始的执行顺序

静态代码块》普通代码块》构造器

9.对final的理解

10.使用final修饰一个变量时,是引用不能改变,引用指向的对象可以改变?

引用不能改变

引用指向的对象实体中的属性,如果没有final修饰,则可以改变

11.final不能用于修饰构造器方法

12. final或static final修饰成员变量,能不能进行++操作

不能

Java中的继承

继承是类和类之间的关系

继承的理解:

自上而下:定义了一个类A,在定义另一个类B时,发现类B的功能与类A相似,考虑类B继承于类A

自下而上:定义B,C,D有类似的属性和,则考虑可以将相同的属性和方法进行抽取,封装到类A中,让类B,C,D继承于类A

语法格式:

1
2
3
class B extends A{

}

B类继承了A类。

注意:java中的继承是单继承,只能有一个父类。但可以多重继承

1.成员变量(属性)

​ 公开的和默认的属性,子类可以继承使用

2.成员方法:

​ 公开和默认的成员方法,子类可以继承使用

总结: 凡是私有的,无法继承。

3.构造方法:

​ 先执行父类构造方法,再执行子类的构造方法

​ 如果父类中有有参构造,但是没有无参构造,并且子类没有调用父类构造器,子类会报错

总结:父类与子类的构造方法的形式需要保持一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

class Father {
public String name;
int age; //默认的属性
private int id;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public Father() {
System.out.println("Constructor Father");
}

public void eat() {
System.out.println("Manger");
}
void work() {
System.out.println("Courir");
}
private void fumer() {
System.out.println("Fumer");
}
public void test() {
fumer();
}
}

class Son extends Father{
public Son() {
System.out.println("Constructor Son");
}
}

public class Demo1{
public static void main(String[] args) {
Son son = new Son();
son.name = "YY";
son.age = 1;
System.out.println(son.name);
System.out.println(son.age);
son.eat();
son.work();
//使用set/get访问父类私有化属性
son.setId(999);
son.test();
}
}

重写override

重写目的:子类可以继承父类的非私有化的方法,有时父类的需求满足不了子类的需求,这时需要重写父类非私有的方法

重写的要求

  • 必须要有继承关系
  • 父类的方法必须是非私有化
  • 在子类中重写的方法,除方法体外其他的都一样
  • 子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符
  • 关于返回值
    -父类的方法的返回值类型是void,则子类重写的返回值必须是void
    -父类返回值为基本数据类型,子类必须是一模一样的基本数据类型
    -父类返回值是引用数据类型(比如类),子类返回值可以与被重写的返回值类型相同或是被重写的方法的返回值类型的子类
  • 关于异常
    -子类重写抛出的异常可以与父类方法的异常类型相同或是异常类型的子类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30

    class Father{
    public void eat() {
    System.out.println("Manger F");
    }
    public void drive() {
    System.out.println("Conduire F");
    }
    }

    class Son extends Father{
    //重写的方法和父类方法的返回值、参数、方法名字一模一样但是方法体不一样。
    @Override //重写的严格限定
    public void eat() {
    System.out.println("Manger Son");
    }

    @Override
    public void drive() {
    System.out.println("drive Son");
    }
    }

    public class Demo1 {
    public static void main(String[] args) {
    Son son = new Son();
    son.eat();
    son.drive();
    }
    }

重载overload

java同一个类中有很多个方法,方法名一样,但参数类型不一样,这就重载

总结:

  • 方法的重载必须在同一个类中
  • 方法名字必须一致
  • 方法的参数类型必须不一致
  • 方法的返回值可以不一样
  • 无参构造和有参构造,也是一种重载

Super关键字

只能用于继承,并在子类中使用。代表父类对象
Case1: 子类继承父类后,对父类方法进行重写,在子类中对父类的方法进行调用
Case2: 子类继承父类后,发现子类和父类中定义类了同名的属性,在子类中区分同名属性

this:

  • ​ 代表当前类对象
  • ​ 可以调用属性和方法
  • ​ 可以调用构造方法

super:

  • ​ 代表父类的对象
  • ​ 可以调用父类的成员变量和成员方法
  • ​ 当super调用属性时,就是内存里面那一个,子类和父类的共有属性都是同一个内存地址
  • ​ 可以调用父类的构造方法

子类对象实例化全过程

1.从结构角度来看:体现为类的继承性

当我们创建子类对象后,子类对象就获取了其父类中声明所有的属性和方法,在权限允许的情况下,可以直接调用

2.从过程角度来看:

当我们通过子类的构造器创建对象时,子类的构造器一定会直接或间接调用其父类的构造器,而其父类的构造器同样会直接或间接的调用其父类的父类构造器…直到调用了Object类中的构造器为止

正因为我们调用过子类所有父类的构造器,所有我们就会将父类中声明的属性、方法加载到内存中,供子类对象使用

抽象

在面向对象中,所有的对象可以通过类来创建。并不是所有类可以通过创建对象。如果一个类中没有足够完整的信息来描述具体的对象的话,那么这个类叫抽象类

abstract修饰

  • 此类成为抽象类
  • 抽象类不能实例化
  • 抽象类中是包含构造器的,因为子类实例化时会直接或间接调用到父类的构造器
  • 抽象类可以没有抽象方法。

abstract修饰方法

  • 此方法即为抽象方法
  • 抽象方法只有方法声明,没有方法体。
  • 子类必须要重写父类的所有抽象方法后才能实例化

abstract不能修饰:属性、构造器、代码块等

abstract不能修饰:私有方法、静态方法、final方法、final类

  • 私有方法不能重写
  • 静态方法不支持重写
  • final的方法不能被重写
  • final修饰的类无子类

对象的多态性

1.如何理解多态性:一个事物的多种形态。

2.Java多态性的体现:

​ 子类对象的多态性:父类的引用指向子类的对象

​ 比如 Peple p = new Man();

3.多态性的应用:

多态性的应用:虚拟方法的调用

在多态场景下,调用方法时:

​ 编译时,认为方法是左边声明的父类的类型的方法

​ 执行时,实际执行的是右边子类重写父类的方法

简称:编译看左边,运行看右边

多态是运行时的行为。

4.多态使用前提:

  • 要有类的继承关系
  • 要有方法的重写

5.多态的实用性

开发中:

使用父类做方法的形参,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,提高了扩展性,符合开闭原则。

开闭原则

  1. 对扩展开放,对修改关闭
  2. 简单来说,软件系统的各种组件,如模块、类、功能等,应该在不修改现有代码基础上,引入新功能。

ex:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Account{
public void withdraw(){} //取钱
}

class CheckAccount extends Account{
@Override
public void withdraw(){} //取钱
}

class SavingAccount extends Account{
@Override
public void withdraw(){} //取钱
}

class Customer{
Account account;

public void setAccount(Account account){
this.account = account;
}

public Account getAccount(){
return account;
}

class Test{
main(){
Customer cust = new Customer();
cust.setAccount(new CheckAccount()); //直接使用子类
cust.withdraw(); //使用的也是子类重写的方法
}
}
}

好处: 减少代码冗余,不需要定义多个重载的方法

弊端:

在多态场景下,创建了子类的对象,也加载了子类特有的属性和方法。但是由于声明为父类的引用导致我们没有办法直接调用子类特有的属性和方法。

向上转型和向下转型

因为多态一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现类型转换的现象。

但是,父类变量接受了子类对象之后,我们就不能调用子类拥有而父类没有的方法了。想要调用子类特有的方法,必须做类型转换,使得编译通过。

VR33I.png

向下转型后就可以调用子类的特有方法。但在转型前,先使用instanceof进行判断

格式: a instanceOf A (判断对象a是否是类A的实例)

Object类

1.类java.lang.Object是类层次结构的根类,即其他所有层次的父类。

  • Object类中没有声明属性

  • Object类提供了一个空参构造器

Equals()

1.适用性:

任何引用数据类型都可以使用

2.子类使用说明:

  • 自定义类在没有重写Object中的equals()方法时,调用的就是Object类中声明的equals(),比较两个对象的引用地址是否相同。(或比较两个对象是否指向了堆空间中的同一个对象实体)

  • 像String、File、Date和包装类等,它们都重写了Object类中的equals()方法,用于比较两个对象的实体内容是否相等

3.开发中使用说明:

实际开发中,针对于自定义的类,常常会判断两个对象是否equals(),而此时主要是判断两个对象的属性值是否相等。所有我们需要重写object类的equals()方法

重写方法:

  • 自己重写

  • 调用IDEA自动生成

4.注意:判断两个对象是否equals(),除了该对象的类需要重写equals之外,其内部的类类型的属性所在的类,也需要重写equals()

5.常见问题:区分 == 和equals()

== :运算符

  1. 使用范围:基本数据类型、引用数据类型
  2. 基本数据类型:判断数据值是否相等

equals():方法

  1. 使用范围:只能使用在引用数据类型上
  2. 具体使用:对于类来说,重写equals()和不重写equals的区别

toString()

Object中的toString()调用后,返回当前对象所属的类和地址值

1.开发中的使用场景

平时在调用system,out.println()打印对象变量时,其实就调用了对象的toString()

2.子类使用说明

  • 自定义的类,在没有重写Object类的toString()的情况下,默认返回的是当前对象的地址

  • 像String、file、Date或包装类等object子类,它们都重写了Object类的toString(),在调用toString()时,返回当前对象的实体内容

3.开发中使用说明