【Java基础回顾】Java基础回顾重难点笔记

一、Java概览

1.Java的运行过程

🍎Java程序运行时,必须经过编译和运行两个步骤。
🍊首先将后缀名为.java的源文件进行编译,最终生成后缀名为.class的字节码文件。然后Java虚拟机将字节码文件进行解释执行,并将结果显示出来
🍉Java程序的运行过程详细解释
🍋1、编写一个HelloWorld.java文件
🍋2、使用javac HelloWorld.java 命令开启Java编译器并进行编译。编译结束后,会自动生成一个HelloWorld.class的字节码文件
🍋3、使用java HelloWorld 命令启动Java虚拟机运行程序,Java虚拟机首先将编译好的字节码文件加载到内存,这个过程被称为类加载,它是由类加载器完成的,然后虚拟机针对加载到内存中的Java类进行解释执行,便可看到运行结果

2.Java的跨平台的解释

Java程序是由虚拟机负责解释执行的,而并非操作系统。
Java程序通过Java虚拟机可以达到跨平台特性,但Java虚拟机并不是跨平台的,也就是说,不同操作系统上的Java虚拟机是不同的,即Windows平台上的Java虚拟机不能使用在Linux平台上,反之亦然

ClassLoader 是 JVM 实现的一部分
Java 编译器会将程序编译为 Java 虚拟机可以执行的字节码
Java 虚拟机(Java Virutal Machine)简称 JVM ,用于执行 Java 字节码
Java 运行时环境(Java Runtime Evironment)简称 JRE ,用于运行 Java 程序,包含 JVM
Java 开发工具包(Java Development Kit)用于开发Java程序,包含JRE和 Java 编译工具等

2.Maven基础

解读pom.xml

<!-- 当前Maven工程的坐标 -->
<groupId>com.atguigu.maven</groupId>
<artifactId>pro01-maven-java</artifactId>
<version>1.0-SNAPSHOT</version>

<!-- 当前Maven工程的打包方式,可选值有下面三种: -->
<!-- jar:表示这个工程是一个Java工程  -->
<!-- war:表示这个工程是一个Web工程 -->
<!-- pom:表示这个工程是“管理其他工程”的工程 -->
<packaging>jar</packaging>

<name>pro01-maven-java</name>
<url>http://maven.apache.org</url>

<properties>
    <!-- 工程构建过程中读取源码时使用的字符集 -->
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<!-- 当前工程所依赖的jar包 -->
<dependencies>
    <!-- 使用dependency配置一个具体的依赖 -->
    <dependency>

        <!-- 在dependency标签内使用具体的坐标依赖我们需要的一个jar包 -->
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>

        <!-- scope标签配置依赖的范围 -->
        <scope>test</scope>
    </dependency>
</dependencies>

二、Java的基本语法

1.Java中的注释

单行注释,用符号 // 表示
多行注释,以符号 /* 开头,并以符号 * / 结尾
文档注释,以符号 /** 开头,并以符号 */ 结尾


注意:
多行注释中 可以 嵌套使用单行注释
多行注释中 不能 嵌套多行注释

2.Java中的关键字

JDK 8 中有 50 个关键字,这些关键字都是小写的

在这里插入图片描述

3.关键字总结

1.abstract

  • 修饰类:
    abstract修饰类,这个类就是抽象类,抽象类中可以有非抽象变量和成员变量,也可以有普通方法、构造方法。但是不能实例化,只能被子类继承。
    如果子类不是抽象类,则必须重写父类的抽象方法。
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
    ...
}
  • 修饰方法:
    abstract修饰方法,这个方法就是抽象方法。抽象方法必须存在于抽象类中。抽象方法不能有具体实现。
abstract public E get(int index);

2.assert

assert表示“断言”,有两种使用方法:

assert 表达式;

若表达式为真,程序继续执行;若表达式为假,则抛出一个AssertionError异常。

assert 表达式:错误信息;

与第一种方法相同,只是异常中带有错误信息。

使用assert时不能在表达式中完成任何程序实际所需的行为(只能做判断)。因为正常发布的代码都是断言无效的,即正常发布的代码中断言语句都不不执行的。

3.boolean

boolean是Java的基本类型之一(默认值false)。只有两个值:true和false。区别C的判断句,Java不能直接使用1和0来表示真假,且boolean类型也不能强转到其他基本类型。

boolean a = true;
boolean b = false;

4.break

  • break在switch中用于跳出switch块,停止switch向下穿透的现象。
case value:expression;
    break;
  • break在循环中用于跳出循环。
while(...){
    ...
    break;
}
  • break也可以在后面接标签,用来跳出一些嵌套比较复杂的循环中。
flag:
for(...){
    for(...){
        break flag;
    }
}

5.byte

byte是Java的基本类型之一(默认值0)。表示8位有符号整数。
范围:-128~127

byte a = 100;

6.case

case用于switch中,用于判断和执行语句。用法:

case 变量值:语句;

若变量值和switch(变量值)中的变量值相等,就执行后面的语句。执行完后继续执行下一个case语句。

7.catch

catch用于捕获异常。
用法:

catch(异常类型 异常){...}

在try/catch语句块中,catch捕获发生的异常,并应对错误做一些处理。
当catch捕获到异常后,try中执行的语句终止,并跳到catch后的语句中。

8.char

char是Java的基本类型之一(默认值\u000)。表示16位、在Unicode编码表中的字符。使用单引号来表示字符常量,例如’A’。
范围:0-65535

9.class

class表示类。用于声明一个类。

[访问控制] (abstract) class 类名 (implements){...}

10.const

const是Java的一个保留关键字,没有实际意义,但是不能用于做变量名(因为被保留作为关键字了)。在C语言中表示常量,类似Java的final。

11.continue

  • continue用于在循环中跳过本次循环。
while(...){
    ...
    continue;
}
  • continue也可以在后面接标签,在一些嵌套比较复杂的循环中跳过一次循环。
flag:
for(...){
    for(...){
        continue flag;
    }
}

12.default

default关键字:

  • 用于switch做默认分支:
default:语句;
  • 用于接口,让接口实现具体的方法:
public interface a{
    default void b(){
        具体方法;
    }
}

default用于接口时,必须要有具体实现。

13. do

do用于和while组成循环,do/while循环不同于while循环,属于先执行循环体再判断。

do{
	循环体;
}while(...)

14.double

double是Java的基本类型之一(默认值0.0d),表示双精度、64位的浮点数。

double a = 0.1d;

15.else

else用于分支结构中的判断。例如:

if(判断1){
    语句1;
}else if(判断2){
    语句2;
}else{
    语句3;
}

16.enum

enum表示枚举,用于限制变量值的类型,例如:

public enum Alpha (implements 接口){
    (public static final)a,b,c,d
}

枚举类中可以有成员变量和方法。

17.extends

extends表示继承。例如:

class 子类 extends父类{}

Java中的继承是单继承,即子类只能有一个直接父类。
除了private,子类可以访问父类的方法和成员变量。

18.final

  • 修饰变量:
    将变量变为常量,在初始化变量后不能再改变值。
  • 修饰方法:
    被final修饰的方法不能被子类重写。
  • 修饰类:
    被final修饰的类不能被继承。

19.finally

finally在try/catch语句块中处理一些后续的工作。例如关闭网络连接和输入输出流等。

如果在try/catch中使用return,则finally会撤销这个return,无论如何都会执行finally中的语句。

20.float

float是Java的基本类型之一(默认值0.0f)。表示单精度、32位的浮点数。

float a = 0.1f;

21.for

for用于循环:

for(初始化循环变量; 判断执行条件;更新循环变量){
    语句
}
for(变量:数组){
    语句
}

22.goto

Java中的保留关键字,没有实际意义,但是不能用做变量名。在C中表示无条件跳转语句。

23.if

if用于分支结构中的判断。常与else和else if使用。

if(表达式){语句}

若表达式为真,则执行后面的语句。

24.implements

implements用于接入接口。接上接口的类必须实现接口的抽象方法(可以不实现默认方法和静态方法)

class A implements B{
    @Override
    do(){
        ...
    }
}

25.import

用于导入包。

26.instanceof

instanceof用于判断类与对象的关系。例如:

a instanceof b

若a是b的一个实例(或子类对象),则整个表达式的结果是true,否则结果为false。

27.int

int是Java的基本类型之一(默认值为0)。表示32位、有符号的整数。
范围:[-231,231-1)

28.interface

interface用于声明一个接口,例如:

public interface A{
    void b();
}

声明A为一个接口,若接上该接口,则必须实现其中的抽象方法b。
接口中的成员变量是static、final、public的。接口中的方法为静态方法或默认方法和静态方法。

29.long

long是Java的基本类型之一(默认值为0L),表示64位、有符号的整数。
范围:[-263,263)
long a = 3216846849646L;

30.native

native可以让Java运行非Java实现的方法。例如c语言,要编译后用javah产生一个.h文件。导入该.h文件并且实现native方法,编译成动态链接库文件。在Java加载动态链接库文件,这个native方法就可以在Java中使用了。

public native void aVoid();

31.new

new用于生成类的实例。

Object a = new Object()

32.package

package用于规定当前文件的包。

package com.example.zhangyijun.testdefactivity;

33.private

访问控制的一种。
私有的方法和变量只能在本类中访问。类和接口不能为私有。

private int a = 1;
private void b(){
    ...
}

34.protected

访问控制的一种。
受保护的方法和变量只能给子类和基类访问。

protected int a = 1;
protected void b(){
    ...
}

35.public

访问控制的一种。
公有的方法、类、变量、接口能够被任何其他类访问。

36.return

方法中返回数据,并结束方法。

37.strictfp

使用strictfp关键字来声明一个类、接口或者方法时,那么该类、接口或者方法会遵循IEEE-754标准来执行,提高浮点运算的精度,并且减少不同硬件平台之间由于浮点运算带来的差异。

public strictfp double aDouble(){
    return 0d;
}

38.short

short是Java的基本类型之一(默认值0),表示16位、有符号的整数。
范围:[-215,215)

short a = 0;

39.static

static修饰的语句块存放在堆的方法区中。

  • 静态变量:依附在类中的变量,可以被类的所有的实例共用。
static int a = 0;
  • 静态方法:依附在类中的方法。静态方法只能访问类中的静态变量和静态方法。
publlic static void b(){
    ...
}
  • 静态块:在类加载的时候执行块中的语句,块中不能访问非静态变量。
static{
    ...
}
  • 静态内部类:用static修饰内部类。

40.super

super即超类

  • 引用父类的的成员:
super.xxx
  • 变量或方法重名时用super调用父类的成员或方法。
  • 调用父类的构造方法:
super(xxx);

41.switch

switch用于分支结构,判断某个变量与一系列值是否相等。switch 语句中的变量类型可以是: byte、short、int 、char、String、enum。

switch(变量){
	case value1:语句1;
		break;
	case value2:语句2;
		break;
	...
	default:语句;
}

若变量和case后的值相等则执行语句。
当语句执行到break时跳到switch块后,如果没有break会产生穿透现象。
default分支必须为最后一个分支,在没有值和case变量相等时执行该语句。

42.synchronized

synchronized关键字用于保证线程安全。由这个关键字修饰的方法或者代码块保证了同一时刻只有一个线程执行该代码。

synchronized(obj){...}

当一个线程访问同步代码块时,检查obj是否有锁,如果有就挂起。如果没有就获得这个obj的锁,也就是把其他线程锁在了外面。当代码执行完毕时释放该锁,其他线程获得锁继续执行代码。

43.this

  • 指向当前对象:this.xxx
  • 形参和成员名字重名时时用this区分。
  • 引用构造函数。

44.throw

用于抛出一个异常。

throw (Exception);

45.throws

在方法中将发生的异常抛出。

[控制访问](返回类型)(方法名)([参数列表])[throws(异常类)]{...}

46.transient

类接上序列化接口后,可以通过transient关键字将某些变量变得无法序列化。

transient int a = 1;

47.try

在try/catch中,将可能出现异常的语句放在try{}块中,出现异常之后代码将会终止并跳到catch中继续执行。

try{
    ...
}catch(Exception e){
    ...
}finally{
    ...
}

48.void

修饰方法,表示方法没有返回值。

49.volatile

volatile关键字修饰的变量在多线程中保持同步。相比synchronized效率要高,不会阻塞线程。但只能保证数据的可见性,不能保证数据的原子性。例如在处理i++的时候另外一个线程修改i的值,那么i的值将会发生错误,这是原子性导致的。

volatile int a;

50.while

while用于两种循环结构:

while(判读语句){
    循环体...
}
do{
	循环体...
}while(判读语句)

3.java中的标识符

在编程过程中,经常需要在程序中定义一些符号来标记一些名称,如包名、类名、方法名、参数名、变量名等,这些符号被称为标识符。标识符可以由任意顺序的大小写字母、数字、下画线(_)和美元符号($ )组成,但标识符不能以数字开头,也不能是Java 中的关键字

4.多态

多态就是同一个行为,使用不同的实例而发生不同的作用。在使用多态调用方法的时候,编译器检查父类中是否有该方法,如果有才能编译通过,例如:

public class Animals{
    void voice(){动物叫}
}

class Cat extends Animals{
    void voice(){猫叫}
}

public static void testVoice(Animals a){
    a.voice();
}

public static void main(String[] args) {
    testVoice(new Cat())Animals a = new Cat();
    a.voice();
}

猫继承自动物这个类,Animals a = new Cat()是向上转型(父类引用指向子类对象),实际的运行时类型还是Cat,也就是说a instanceof Cat 表达式为真,因此调用a的voice()方法是猫叫。结合C的指针和内存分析来理解多态。

5.泛型

  • 类型通配符
<? extends T>表示该通配符所代表的类型是T类型的子类。
<? super T>表示该通配符所代表的类型是T类型的父类
public static <T extends Closable> void close(T... a){
    for(T temp:a){
        try{
            if(temp!=null){
                temp.close();
            }
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}
  • 泛型不能用在静态属性上
  • 指定的类型不能为基本类型

6.反射

  • 获取目标类型的Class对象
Class<?> a = Object.getClass();
Class<?> b = T.class;
Class<?> c = Class.forName(...);
  • 通过 Class 对象分别获取Constructor类对象、Method类对象 & Field 类对象

不带 "Declared"的方法支持取出包括继承、公有(Public) & 不包括有(Private)的构造函数
带"Declared"的方法是支持取出包括公共(Public)、保护(Protected)、默认(包)访问和私有(Private)的构造方法,但不包括继承的构造函数

Constructor

//a.获取指定的构造函数(公共/继承)
Constructor<T> getConstructor(Class<?>... parameterTypes);
//b.获取所有的构造函数(公共/继承) 
Constructor<?>[] getConstructors(); 
//c.获取指定的构造函数(不包括继承)
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes);
//d.获取所有的构造函(不包括继承)
Constructor<?>[] getDeclaredConstructors(); 

Field

//a.获取指定的属性(公共/继承)
Field getField(String name);
//b.获取所有的属性(公共/继承)
Field[] getFields();
//c.获取指定的所有属性(不包括继承)
Field getDeclaredField(String name);
//d.获取所有的所有属性(不包括继承)
Field[] getDeclaredFields();

Method

//a.获取指定的方法(公共/继承)
Method getMethod(String name, Class<?>... parameterTypes);
//b.获取所有的方法(公共/继承)
Method[] getMethods();
//c.获取指定的方法 (不包括继承)
Method getDeclaredMethod(String name, Class<?>... parameterTypes);
//d.获取所有的方法(不包括继承)
Method[] getDeclaredMethods();

7.集合

  • 迭代器遍历ArrayList
Iterator<String> iterator = list.iterator();
while(ite.hasNext()){
    Log.d("TAG",ite.next());
}
  • 遍历Map
//第一种:map.keySet()
for (String key : map.keySet()) {
    System.out.println("key= "+ key + " value= " + map.get(key));
}

//第二种:map.entrySet().iterator()
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<String, String> entry = it.next();
    System.out.println("key= " + entry.getKey() + " value= " + entry.getValue());
}
      
//第三种:map.entrySet()
for (Map.Entry<String, String> entry : map.entrySet()){
    System.out.println("key= " + entry.getKey() + " value= " + entry.getValue());
}
    
//第四种:map.values()
for (String v : map.values()) {
    System.out.println("value= " + v);
}

1.遍历 hashMap() 时 entrySet() 方法是将 key 和 value 全部取出来,所以性能开销是可以预计的, 而 keySet() 方法进行遍历的时候是根据取出的 key 值去查询对应的 value 值, 所以如果 key 值是比较简单的结构(如1,2,3…)的话性能消耗上是比 entrySet() 方法低, 但随着 key 值得复杂度提高 entrySet() 的优势就会显露出来。
2.在只遍历 key 的时候使用 keySet(), 在只遍历 value 的时候使用 values(), 在遍历 key-value的时候使用 entrySet()。

8.正则

  • 转义字符
\n  \t  \\  \^  \$  \(  \)  \{  
\}  \?  \+  \*  \|  \[  \]
  • 标准字符集合(大写取反)
    在这里插入图片描述

9.引用分类

强引用:StrongReference:引用指向对象,gc运行时不回收
软引用:SoftReference:gc运行时回收,(jvm内存不够)
弱引用:WeakReference:gc运行时立即回收
虚引用:PhantomReference:跟踪对象被回收的状态,必须与ReferenceQueue一起使用

10.Java八种数据类型

在这里插入图片描述

三、集合

在这里插入图片描述
Java集合框架
在这里插入图片描述
使用场景
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

文章参考:
参考一
参考二

1.List

ArrayList
底层基于数组实现,实现了长度可变的数组,其最大特点是内存空间连续,优点是遍历元素和随机访问效率比较高;

ArrayList特点:
①数据是插入有序的
②数据是可以重复的
③可以存储null
④底层数据结构是数组
⑤可以动态扩容的,默认容量是10
⑥扩容是按照原大小的1.5倍进行扩容

ArrayList应用场景:
在查询操作的时间复杂度O(1),对于数据操作时间复杂度是O(n),ArrayList的适用于多查询、修改较少的业务场景

LinkedList
底层基于双向链表实现,其最大特点是内存空间不连续,其中元素允许为null,优点是增加元素和删除元素效率比较高;LinkedList类是双向列表,列表中的每个节点都包含了对前一个和后一个元素的引用.

LinkedList接口特点:
1、数据插入有序
2、数据可重复
3、可以存储null
4、底层数据结构是链表

ArrayList和LinkedList区别:
①ArrayList基于动态数组,LinkedList基于链表。
②对于增加元素和删除元素LinkedList效率比较高,因为ArrayList要移动元素。
③对于遍历元素和随机访问ArrarList效率比较高,因为LinkedList要移动指针。

Vector
底层是数组,是线程安全的(synchronized)重量级锁

2.Set

Set接口特点
Set接口是无序的
Set接口中的数据不允许重复
Set接口无法通过下标访问数据
查找慢,插入删除快(底层数据结构是哈希表和红黑树)
Set集合使用equals()和hashCode()方法实现元素去重

HashSet特点:
HashSet是Set接口的实现类
线程不安全

TreeSet
TreeSet 基于TreeMap 实现。TreeSet可以实现有序集合,但是有序性需要通过比较器实现。

TreesSet特点:
有序
不重复
添加、删除、判断元素存在性效率比较高
线程不安全

TreeSet对元素进行排序的方式:
1)如果是基本数据类型和String类型,无需其它操作,可以直接进行排序。
2)对象类型元素排序,需要实现Comparable接口,并覆盖其compareTo方法。
3)自己定义实现了Comparator接口的排序类,并将其传给TreeSet,实现自定义的排序规则。

LinkedHashSet
LinkedHashSet是一种有序的Set集合,其元素的存入和取出顺序是相同的。

3.Map

Map集合的特点
Map并不是集合,而表示两个集合之间的一种关系,故Map没有实现Collection接口。
A集合应该是一个Set集合,B集合应该是List集合。
我们把A集合中的元素称之为key,把B集合中的元素称之为value。
Map其实就有多个key-value(键值对)组成的,每一个键值对我们使用Entry表示,Map结构也可以理解为是Entry的集合,即Set< Entry >。


Map集合的遍历方式

  • 1.keySet方法或者values方法
 public void test() {
     Map map = new HashMap(); 
     // KeySet 获取key  
     for (Integer key : map.keySet()) {
         System.out.println(key);
     }
     // values 获取value
     for (Integer value : map.values()) {
         System.out.println(value);
     }     
 }
  • 2.利用keySet get(key)方法
 // keySet get(key) 获取key and value
 public void testKeySetAndGetKey() {
     for (Integer key : map.keySet()) {
         System.out.println(key + ":" + map.get(key));
     }
 }
  • 3.利用entrySet方法
 // entrySet 获取key and value
 public void testEntry() {
     for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
         System.out.println(entry.getKey() + ":" + entry.getValue());
     }
 }
  • 4.Iterator迭代器
 public void testIterator() {
     Iterator<Map.Entry<Integer, Integer>> it = map.entrySet().iterator();
     while (it.hasNext()) {
         Map.Entry<Integer, Integer> entry = it.next();
             System.out.println(entry.getKey() + ":" + entry.getValue());
     }
 }

HashMap & Hashtable
HashMap 和 Hashtable 是 Map 接口的两个典型实现类。
Hashtable、HashMap 也不能保证其中 key-value 对的顺序
Hashtable 、HashMap 判断两个 Key 相等的标准是:两个Key 通过 equals 方法返回 true,hashCode 值也相等。
Hashtable 、HashMap 判断两个Value相等的标准是:两个 Value 通过 equals 方法返回 true


区别:
Hashtable 是一个古老的 Map 实现类,不建议使用。
Hashtable 是一个线程安全的 Map 实现,但HashMap 是线程不安全的。
Hashtable不允许使用 null 作为 key 和 value,而 HashMap 可以。


HashMap的底层:
数组+链表(jdk7之前)
数组+链表+红黑树(jdk8)

TreeMap
TreeMap底层基于红黑树算法,因为Map中的key是Set,所以不能保证添加的先后顺序,也不允许重复,但是Map中存储的key会默认使用自然排序(从小到大),和TreeSet一样,除了可以使用自然排序也可以自定义排序。

LinkedHashMap
LinkedHashMap 是 HashMap 的子类.
LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致

四、类和接口

1.抽象类

抽象类概念
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。

抽象类语法
在Java中,一个类如果被 abstract 修饰称为抽象类,抽象类中被 abstract 修饰的方法称为抽象方法,抽象方法不用给出具体的实现(注意:抽象类也是类,内部可以包含普通方法和属性,甚至构造方法)


1.抽象类不能直接实例化对象
2.抽象方法不能是 private 的(注意:抽象方法没有加访问限定符时,默认是public)
3.抽象方法不能被final和static修饰,因为抽象方法要被子类重写
4.抽象类必须被继承,并且继承后子类要重写父类中的抽象方法,否则子类也是抽象类,必须要使用 abstract 修饰
5.抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类
6.抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量

2.匿名类

匿名类概念
匿名类可以使你的代码更加简洁 (JDK8之后Lambda更简洁)。 你可以定义一个类的同时对其进行实例化。
它与局部类很相似,不同的是它没有类名,如果某个局部类你只需要使用一次,就可以使用匿名类代替局部类。 匿名类是表达式,而非常规的类

匿名类的使用场景:
一个局部类只需要使用一次的时候 由于匿名类没有类名,那么除了定义它的地方,其他地方无法调用,所以匿名类也可以叫匿名内部类

五、IO

按数据流向:输入流,输出流( 输入流为读数据,输出流为写数据)

按数据类型:字节输入流/输出流,字符输入流/输出流

字节流抽象类基类:
1、InputStream:这个抽象类是表示字节输入流的所有类的超类
2、OutputStream:这个抽象类是表示字节输出流的所有类的超类
3、子类名特点:子类名称都是以其父类名作为子类名的后缀

1.对象序列化

Java的对象序列化是将Java对象转换成字节序列,二进制流。
这些字节序列可以保存在磁盘,或通过网络传输。并且可以将这种二进制流恢复成原来的Java对象。

对象的序列化(Serialize)是将JAVA对象写入IO流。
反序列化(Deserialize)是从IO流中恢复该java对象

六、单元测试、反射和注解

1.单元测试

测试分类:
1,黑盒测试:不需要写代码,给输入值,看程序是否能够输出期望的值。
2,白盒测试:需要写代码的。关注程序具体的执行流程。

Junit使用:白盒测试

2.反射

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象.

反射就是把java类中的各种成分映射成一个个的Java对象

反射:将类的各个组成部分封装为其他对象,这就是反射机制

1,可以在程序运行过程中,操作这些对象。
2,可以解耦,提高程序的可扩展

3.注解

注解:用来描述注解的注解(jdk定义好的)

​                         @Target:描述注解能够作用的位置

​                               ElementType取值:

​                                  TYPE:可以作用于类上

​                                  METHOD:可以作用于方法上

​                                  FIELD:可以作用于成员变量上

​                         @Retention:描述注解被保留的阶段
     @Retention(RetentionPolicy.RUNTIME):当前被描述的注解,会被保留到class字节码文件中,并被JVM读取到。

                        @Documented:描述注解是否被抽取到api文档中     

​                       @Inherited:描述注解是否被子类继承

七、线程与进程

参考

1.初识多线程

1.1 并行、并发、串行

  • 并发
      多个任务在同一个CPU核上,按细分的时间片轮流执行,从逻辑上来看任务是同时执行。
  • 并行
      单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
  • 串行
      有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。
线程上下文切换,简单来说,指的是CPU保存现场,执行新线程,恢复现场,继续执行原线程的一个过程

多线程编程的实质就是将任务的处理方式由串行改成并发。

1.2 并发编程的优缺点

1.2.1 并发编程的优点
  • 1、充分利用多核CPU的计算能力
      可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的,采用多线程的方式去同时完成几件事情而不互相干扰。
  • 2、方便进行业务拆分,提升应用性能
      多线程并发编程是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
1.2.2 并发编程的缺点
  • 1、频繁的上下文切换
      时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,达到一种"不同应用似乎是同时运行的错觉",时间片一般是几十毫秒。每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势。
  • 2、产生线程安全问题
      即死锁、线程饥饿等问题。

1.3 上下文切换

一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是交替地为每个线程分配时间片。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就是上下文切换

概括来说:当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

在时间片切换到别的任务和切换到当前任务的时候,操作系统需要保存和恢复相应线程的进度信息。这个进度信息就是上下文,它一般包括通用寄存器的内容和程序计数器的内容。

1.4 并发编程三要素

  • 线程安全
      一般而言,如果一个类在单线程环境下能正常运行,并且在多线程环境下也能正常运行,那么就称其是线程安全的。
  • 非线程安全
      一个类在单线程情况下能正常运行,但在多线程环境下无法正常运行,那么这个类就是非线程安全的。
      
    线程安全问题概括来说表现为3个方面:原子性、可见性和有序性。

1.5 同步与异步

  • 同步
      当一个同步调用发出去后,调用者要一直等待调用结果的返回后,才能进行后续的操作。
  • 异步
      当一个异步调用发出去后,调用者不用管被调用方法是否完成,都会继续执行后面的代码。 异步调用,要想获得结果,一般有两种方式:

1.主动轮询异步调用的结果;
2.被调用方通过callback来通知调用方调用结果(常用)。

1.6 进程与线程

进程是程序向操作系统申请资源的基本单位,线程是进程中可独立执行的最小单位。通常一个进程可以包含多个线程,至少包含一个线程,同一个进程中所有线程共享该进程的资源。

  • 1、根本区别
      进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
  • 2、资源开销
      每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
  • 3、包含关系
      一个进程里可以包含多个线程。
  • 4、内存分配
      同一进程的线程共享本进程的地址空间和资源,而线程之间的地址空间和资源是相互独立的
  • 5、影响关系
      一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
  • 6、执行过程
      每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。

1.7 线程调度

一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。多线程的并发运行,指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。
  
  线程调度模型有两种:

  • 1、分时调度模型
      分时调度模型是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片。
  • 2、抢占式调度模型
      抢占式调度模型是指优先让运行池中优先级高的线程占用CPU,如果运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。
      
      Java虚拟机(JVM)采用抢占式调度模型

2.线程的基本使用

在Java中创建一个线程,可以理解创建一个Thread类(或其子类)的实例。线程的任务处理逻辑可以在Thread类的run实例方法中实现,运行一个线程实际上就是让Java虚拟机执行该线程的run方法。run方法相当于线程的任务处理逻辑的入口方法,它由虚拟机在运行相应线程时直接调用,而不是由相应代码进行调用。
  
  启动一个线程的方法是调用start方法,其实质是请求Java虚拟机运行相应的线程,而这个线程具体何时运行是由线程调度器决定的。因此,start方法调用结束并不意味着相应线程已经开始运行。

2.1 创建线程

创建线程有4种方式。

  • 1.继承Thread类

使用方式:
继承Thread类;
重写run方法;
创建Thread对象;
通过start()方法启动线程。

  • 2.实现Runnable接口

使用方式:
实现Runnable接口;
重写run方法;
创建Thread对象,将实现Runnable接口的类作为Thread的构造参数;
通过start()进行启动。

  • 3.实现Callable接口

前两种方式比较常见,Callable的使用方式
创建实现Callable接口的类;
以Callable接口的实现类为参数,创建FutureTask对象;
将FutureTask作为参数创建Thread对象;
调用线程对象的start()方法。

  • 4.使用Executors工具类创建线程池
      由于线程的创建、销毁是一个比较消耗资源的过程,所以在实际使用时往往使用线程池。
      在创建线程池时,可以使用现成的Executors工具类来创建,该工具类能创建的线程池有4种:newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool。此处以newSingleThreadExecutor为例,其步骤为

使用Executors类中的newSingleThreadExecutor方法创建一个线程池;
调用线程池中的execute()方法执行由实现Runnable接口创建的线程;
或者调用submit()方法执行由实现Callable接口创建的线程;
调用线程池中的shutdown()方法关闭线程池。

2.2 4种创建方式对比

1、继承Thread类
  优点 :代码简单 。
  缺点 :该类无法继承别的类。
2、实现Runnable接口
  优点:继承其他类,同一实现该接口的实例可以共享资源。
3、实现Callable接口
  优点:可以获得异步任务的返回值。
4、线程池
  优点:实现自动化装配,易于管理,循环利用资源。

2.3 线程的 run()和 start()有什么区别

两者的区别:

start()方法用于启动线程,run()方法用于实现具体的业务逻辑。
run()可以重复调用,而start()只能调用一次。

调用start()方法来启动一个线程,无需等待run()方法体代码执行完毕,可以直接继续执行其他的代码,此时线程是处于就绪状态,并没有运行。

2.4 守护线程和用户线程

Java 中的线程分为两种:守护线程和用户线程。任何线程都可以设置为守护线程和用户线程,通过方法setDaemon(true)可以把该线程设置为守护线程,反之则为用户线程。
  用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程。
  守护线程:运行在后台,为其他前台线程服务,比如垃圾回收线程,JIT(编译器)线程就可以理解为守护线程。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作。
  守护线程应该永远不去访问固有资源 ,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

注意事项
1.setDaemon(true)必须在start()方法前执行,否则会抛出IllegalThreadStateException
2.在守护线程中产生的新线程也是守护线程。
3.不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑。
4.守护线程中不能依靠finally块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作,所以守护线程中的finally语句块可能无法被执行。

2.5 线程的生命周期

2.5.1 从代码角度理解

在Thread类中,线程状态是一个枚举类型:

   public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED
    }

线程的状态可以通过public State getState()来获取,该方法的返回值是一个枚举类型,线程状态定义如下:

  • 1、NEW
      一个已创建而未启动的线程处于该状态。
  • 2、RUNNABLE
      该状态可以被看成一个复合状态。它包括两个子状态:READY和RUNNING。前者表示处于该状态的线程可以被线程调度器进行调度而使之处于RUNNING状态,后者表示线程正在运行状态。
      执行Thread.yield()的线程,其状态可能由RUNNING转换为READY。
  • 3、BLOCKED
      处于BLOCKED状态的线程并不会占处理器资源,当阻塞式IO操作完成后,或线程获得了其申请的资源,状态又会转换为RUNNABLE。
  • 4、WAITING
      一个线程执行了某些特定方法之后就会处于这种等待其他线程执行另外一些特定操作的状态。
      能够使线程变成WAITING状态的方法包括:Object.wait()、Thread.join(),能够使线程从WAITING状态变成RUNNABLE状态的方法有:Object.notify()、Object.notifyAll()。
  • 5、TIMED_WAITING
      该状态和WAITING类似,差别在于处于该状态的线程并非无限制地等待其他线程执行特定操作,而是处于带有时间限制的等待状态。
      当其他线程没有在特定时间内执行该线程所期待的特定操作时,该线程的状态自动转换为RUNNABLE。
  • 6、TERMINATED
      已经执行结束的线程处于该状态。
      Thread.run()正常返回或由于抛出异常而提前终止都会导致相应线程处于该状态。
      
    6种状态的转换:
    在这里插入图片描述
2.5.2 从使用角度理解

在实际开发中,往往将线程的状态理解为5种:新建、可运行、运行、阻塞、死亡。
  在这里插入图片描述

  • 1、新建(new)
      新创建了一个线程对象。用new方法创建一个线程后,线程对象就处于新建状态。
  • 2、可运行(runnable)
      线程对象创建后,当调用线程对象的start()方法,该线程处于就绪状态,但还没分配到CPU,处于线程就绪队列,等待系统为其分配CPU。
  • 3、运行(running)
      可运行状态(runnable)的线程获得了CPU时间片,执行程序代码。
      就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
      如果在给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态。
  • 4、阻塞(block)
      处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪(runnable)状态,才有机会再次被CPU调用以进入到运行状态。
      
      阻塞的情况分三种:

等待阻塞   
运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列中,使本线程进入到等待阻塞状态;
同步阻塞   
线程在获取synchronized 同步锁失败(因为锁被其它线程所占用),则JVM会把该线程放入锁池中,线程会进入同步阻塞状态;
其他阻塞
  通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当
sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。

  • 5、死亡(dead)
      线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。

2.6 线程相关的一些问题

2.6.1 interrupt、interrupted和isInterrupted方法的区别

interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态
  线程中断仅仅是设置线程的中断状态标识,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态标识被置为“中断状态”,就会抛出中断异常。
interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。
isInterrupted:查看当前中断信号是true还是false

2.6.2 sleep方法和yield方法有什么区别
  • 1、sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
  • 2、线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
  • 3、sleep()方法声明抛出 InterruptedException(其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException),而 yield()方法没有声明任何异常;
  • 4、sleep()方法比 yield()方法具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行;
  • 5、建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性。
2.6.3 线程怎么处理异常

如果线程运行中产生了异常,首先会生成一个异常对象。我们平时throw抛出异常,就是把异常交给JVM处理。JVM首先会去找有没有能够处理该异常的处理者(首先找到当前抛出异常的调用者,如果当前调用者无法处理,则会沿着方法调用栈一路找下去),能够处理的调用者实际就是看方法的catch关键字,JVM会把该异常对象封装到catch入参,允许开发者手动处理异常。

若找不到能够处理的处理者(实际就是没有手动catch异常,比如未受检异常),就会交该线程处理;JVM会调用Thread类的dispatchUncaughtException()方法,该方法调用了getUncaughtExceptionHandler(),uncaughtExceptoin(this,e)来处理了异常,如果当前线程设置了自己的UncaughtExceptionHandler,则使用该handler,调用自己的uncaughtException方法。如果没有,则使用当前线程所在的线程组的Handler的uncaughtExceptoin()方法,如果线程中也没有设置,则直接把异常定向到System.err中,打印异常信息(控制台红色字体输出的异常就是被定向到System.err的异常)。

2.6.4 Thread.sleep(0)的作用是什么

由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

2.6.5 一个线程如果出现了运行时异常会怎么样

如果这个异常没有被捕获的话,这个线程就停止执行了。
另外重要的一点是:如果这个线程持有某个对象的监视器,那么这个对象监视器器会被立即释放。

2.6.6 终止线程运行的几种情况
  • 线程体中调用了yield方法让出了对CPU的占用权利;
  • 线程体中调用了sleep方法使线程进入睡眠状态;
  • 线程由于IO操作受到阻塞;
  • 另外一个更高优先级线程出现,导致当前线程未分配到时间片;
  • 在支持时间片的系统中,该线程的时间片用完。
  • 使用stop方法强行终止,但是不推荐这个方法,因为stop是过期作废的方法。
  • 使用interrupt方法中断线程。

3 死锁

死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去

3.1 死锁的产生条件

当线程产生死锁时,这些线程及相关的资源将满足如下全部条件:

  • 1、互斥
      一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放。
  • 2、请求与保持(不主动释放)条件
      一个线程(进程)因请求被占用资源(锁)而发生阻塞时,对已获得的资源保持不放。
  • 3、不剥夺(不能被强占)条件
      线程(进程)已获得的资源,在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 4、循环等待(互相等待)条件
      当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞。

用一句话该概括:
  两个或多个线程持有并且不释放独有的锁,并且还需要竞争别的线程所持有的锁,导致这些线程都一直阻塞下去。

这些条件是死锁产生的必要条件而非充分条件,也就是说只要产生了死锁,那么上面的这些条件一定同时成立,但是上述条件即便同时成立也不一定产生死锁。
  
如果把锁看作一种资源,这种资源正好符合“资源互斥”和“资源不可抢夺”的要求。那么,可能产生死锁的特征就是在持有一个锁的情况下去申请另外一个锁,通常是锁的嵌套。

3.2 死锁的规避

       由上文可知,要产生死锁需要同时满足四个条件,所以,只要打破其中一个条件就可以避免死锁的产生。常用的规避方法有如下几种:

  • 1、粗锁法
      用一个粒度较粗的锁替代原来的多个粒度较细的锁,这样涉及的线程都只需要申请一个锁从而避免了死锁。粗锁法的缺点是它明显降低了并发性并可能导致资源浪费。
  • 2、锁排序法
      相关线程使用全局统一的顺序申请锁。假设有多个线程需要申请锁(资源),那么只需要让这些线程依照一个全局(相对于使用这种资源的所有线程而言)统一的顺序去申请这些资源,就可以消除“循环等待资源”这个条件,从而规避死锁。一般,可以使用对象的hashcode作为资源的排序依据。
  • 3、使用ReentrantLock.tryLock(long timeout, TimeUnit unit) 申请锁
      ReentrantLock.tryLock(long timeout, TimeUnit unit) 允许为申请锁这个操作加上一个超时时间。在超时事件内,如果相应的锁申请成功,该方法返回true。如果在tryLock执行的那一刻相应的锁正在被其他线程持有,那么该方法会使当前线程暂停,直到这个锁申请成功(此时该方法返回true)或者等待时间超过指定的超时时间(此时该问题返回false)。因此,使用ReentrantLock.tryLock(long timeout, TimeUnit unit) 来申请锁可以避免一个线程无限制地等待另外一个线程持有的资源,从而最终能够消除死锁产生的必要条件中的“占用并等待资源”。
  • 4、使用开放调用----在调用外部方法时不加锁
      开放调用是一个方法在调用方法(包括其他类的方法以及当前类的可覆盖方法)的时候不持有任何锁。显然,开放调用能够消除死锁产生的必要条件中的“持有并等待资源”。
  • 5、使用锁的替代品
      使用一些锁的替代品(无状态对象、线程特有对象以及volatile关键字等),在条件允许的情况下,使用这些替代品在保障线程安全的前提下不仅能够避免锁的开销,还能够直接避免死锁。

3.3 线程饥饿和活锁

线程饥饿是指一直无法获得其所需的资源而导致任务一直无法进展的一种活性故障。

线程饥饿的一个典型例子是在争用的情况下使用非公平模式的读写锁。此种情况下,可能会导致某些线程一直无法获取其所需的资源(锁),即导致线程饥饿。
  
把锁看作一种资源的话,其实死锁也是一种线程饥饿。死锁的结果是故障线程都无法获得其所需的全部锁中的一个锁,从而使其任务一直无法进展,这相当于线程无法获得其所需的全部资源(锁)而使得其任务一直无法进展,即产生了线程饥饿。由于线程饥饿的产生条件是一个(或多个)线程始终无法获得其所需的资源,显然这个条件的满足并不意味着死锁的必要条件(而不是充分条件)的满足,因此线程饥饿并不会导致死锁。
  
  Java中导致饥饿的原因:

1.高优先级线程抢占了所有的低优先级线程的 CPU 时间。
2.线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
3.线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。

线程饥饿涉及的线程,其生命周期不一定就是WAITING或BLOCKED状态,其状态也可能是RUNNING(说明涉及的线程一直在申请宁其所需的资源),这时饥饿就会演变成活锁。
  
活锁指线程一直处于运行状态,但是其任务却一直无法进展的一种活性故障。

3.4 死锁与活锁的区别,死锁与饥饿的区别

死锁
  是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

活锁
  任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,却一直获得不了锁。

饥饿
  一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。

  • 1、活锁与死锁的区别
      活锁和死锁的区别在于:处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。活锁可以认为是一种特殊的饥饿。

  • 2、死锁、活锁与饥饿的区别
      进程会处于饥饿状态是因为持续地有其它优先级更高的进程请求相同的资源。不像死锁或者活锁,饥饿能够被解开。例如,当其它高优先级的进程都终止时并且没有更高优先级的进程强占资源。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值