Java

Zephyr Lv3

一份关于Java基本操作的笔记

Java随记

面向过程编程

同一个模块或不同模块的函数能够不受限制的访问全局性数据,全局数据和函数之间缺乏联系

面向对象编程

1.定义

在程序中包含各种独立而又互相调用的对象的思想。将数据与对数据的操作封装到一个数据类型之中,该数据类型又可以与其他数据类型进行交互。

2.优势

  • 加大封装力度 若在程序投入使用后,部分数据结构必须改变,那么受影响的也只是对象内部的逻辑。
  • 提升复用性
  • 提高设计效率

3.类与对象

Classes:定义了一件事物的抽象特点。包含了数据以及对数据的操作

Objects:类的实例

4.类与类之间的关系

  • 依赖关系:一个类使用或知道另一个类。是一种瞬时关系,依赖类和目标类进行简单的交互
  • 关联关系:一个类在很长一段时间内都会与另一个类有合作关系
    • 关联关系的三要素:关联的方向、关联的数量、关联的属性
  • 继承关系

5.对象的创建

对象变量的声明:在栈中为对象变量分配一个存储地址数据的内存空间

对象的创建:在堆中分配对象所占的空间,对象变量指向该地址

对象的销毁:当程序中不存在该对象的引用时,就会销毁

Java包

包用于组织、管理类,并且解决类命名冲突问题

若一个源文件中不加package语句,则指定为缺省包,类文件位于当前工作目录下

三元运算符

三元运算符所在表达式会成为一个整体,其精度会由式中最高精度决定。

可变参数

语法: ReturnType funName(TypeName…varName)
本质是数组
可变参数必须在参数列表的最后一个,并且只能有一个可变参数。

super关键字

1.访问父类的属性但不能访问父类的私有属性:super.属性名/方法名
2.访问父类的的构造器:super(参数列表)

动态绑定机制

当调用对象方法时,该方法会和该对象的内存地址/运行类型绑定
当调用对象属性时,没有动态绑定.

类的五大成员:属性,方法,代码块,构造器,内部类

代码块

static代码块作用就是对类进行初始化,并且它随着类的加载而执行,并且只执行一次,而普通代码块每创建一个对象就会执行(可以理解为它和构造函数绑定)

类什么时候被加载
1.创建对象实例的时候(new)
2.子类被加载时,父类也会被加载。
3.使用静态成员时(静态属性和方法) PS:调用静态成员时不会执行普通代码块中的内容

内部类

定义:一个类的内部又完整的嵌套了另一个类结构,被嵌套的类称为内部类。
特点:可以直接访问私有属性,并且可以体现类与类之间的包含关系。

内部类的构造器的第一个参数会被显示指定为外部类实例(锚定)

分类:
1.定义在外部类局部位置上(比如方法内):
1)局部内部类(有类名)
2)匿名内部类(没有类名)
2.定义在外部类的成员位置上:
1)成员内部类(没用static修饰)
2)静态内部类(用static修饰)

局部内部类,匿名内部类的实质是一个对象,一个局部变量,因此不能被外部其他类访问。

局部内部类

1.可以直接访问外部类的所有成员(包含所有的)
2.不能被修饰符所修饰,因为他本质是一个局部变量
3.作用域:仅在定义它的方法与代码块中。
4.外部其他类不能访问局部内部类。
5.若外部类和局部内部类的成员重名时,采取就近原则,若要调用外部类成员则要使用
外部类名.this.成员 (re:对应成员并非静态变量,需要用对象来调用,此时this相当于指向该外部类的当前对象)

匿名内部类

语法:new 类或接口(参数列表){具体实现} (若实现某个接口则不需要传入参数列表,参数列表会被传递给构造器。) PS:生成的匿名内部类的运行类型不是实现的类或接口。
使用环境:类或接口中的抽象方法需要被实现来使用或是要重载重写,但使用次数极少,此时使用匿名内部类可避免额外再创建一个类。

成员内部类

外部其他类访问内部类:
1.外部类.内部类 name = 外部类对象.new 内部类构造函数
2.在外部类中写一个获取内部类的方法
3.作用域:整个类体

静态内部类

1.可以访问任意静态外部类成员
2.外部其他类访问静态内部类:外部类.内部类 name = new 外部类.内部类构造函数 (静态内部类不需要用外部类的对象来调用,因此其构造函数可用外部类名调用)

接口(纯虚基类)

1.接口不能被实例化
2.接口中所有的方法是public抽象方法, 其中abstract可以省略
3.一个普通类实现接口,就必须将该接口中的所有方法都实现,而抽象类可以不必全部实现
4.接口中的属性都是final的, 且具体来说是public static final类型的 eg:int a -> public static final int a 即接口中的属性通过接口名来访问。
5.接口只能继承其他接口,而非实现其他接口

接口与继承的区别

继承的价值主要在于:解决代码的复用性和可维护性
接口的价值主要在于:设计,设计好各种规范(方法),让其他类去实现这些方法,更加灵活
继承满足的是is-a的关系,而接口满足的是like-a的关系
接口在一定程度上实现代码解耦。

枚举类

关键字:enum
enum的代码块中包含:枚举类对象,枚举类的构造函数,方法,属性,代码块
其中枚举类的对象必须声明再最开始,且其中每一个对象默认被public static final修饰
方法compareTo()会比较两个枚举量的编号
方法ordinal()返回编号
构造器默认为私有,且只能为私有
会隐式继承Enum类,因此不能继承其他类。

注解

注解用于修饰解释 包、类、方法、属性、构造器、局部变量等数据信息。
注解不影响程序逻辑,但注解可以被编译或运行

@Override注解:表明以下方法是被重写的,编译器编译时会去父类搜索是否有同名方法,若检测到没有该方法,则会报错。
作用域:方法

@Deprecated注解:表明以下内容已经过时,不推荐使用
作用域:方法、类、字段、包、参数等等

@SupressWarnings注解:抑制编译器警告
语法:@SupressWarnings({要抑制的警告})
作用域:方法、类、字段、包、参数等等

包装类

装箱:底层是valueOf()
拆箱:底层是XXXValue()

PS:valueOf()底层原理:程序会缓存一个-128-127的数组,因此在这个范围内的数不会重新创建一个对象,但对于超出该范围的数则会返回一个new的对象

String类(不可修改,但复用率高)

String类实现了Serializable(序列化), Comparable(可比较)接口。
字符串使用Unicode编码,每个字符占两个字节。
String类用一个value数组存储字符序列,并且它被final修饰,即其地址不可修改
re:数组本身是一个地址,因此final其实修饰了一个地址,即String的value中的值可以改变,但其地址不可变。

String的赋值方法:
String s1 = “abc” 底层原理:变量s1入栈,然后去常量池中寻找是否有”abc”,若没有,则在常量池中创建,然后s1指向常量池中目标的地址。
String s2 = new String(“abc”) 底层原理:变量s2入栈,然后在堆中开辟一块空间用于维护对象,s2指向该空间,然后去寻找常量池中是否有”abc”,若有,则value直接指向它,若没有,则在常量池中先创建,然后value再指向。

intern()方法:若池中已存在一个等于String对象value值(用equal比较)的对象,则返回常量池中的地址,若没有则在池中创建一个再返回。

String a = “a” + “b” 会被编译器优化为:String a = “ab”

String a = “a”; String b = “b”; String c = a + b; 先创建一个StringBuilder类,然后调用两次append() [分别把字符串a和字符串b连入SB对象],最后返回一个字符串对象。
最后 a->堆中对象->池中对象

String每次重新赋值都会重新开辟空间
String本身不可像数组一样访问,要用charAt进行访问。
substring的参数为(int beginindex, int lastindex)前闭后开
replace对调用对象没有影响,它会返回一个新的字符串。
compareTo:若两字符串长度不同且在较小长度范围内相等,则返回长度差值,否则会返回第一组不相同的字符的ASCII码差值。
format静态方法(和printf用法类似)

StringBuffer类(可增删,效率较高,线程安全)

StringBuffer的直接父类是AbstractStringBuilder
在父类中有属性char[] value,不是final修饰的,因此该value指向一个存放在堆中的字符串
StringBuffer是一个final类。

String -> StringBuffer

1.StringBuffer strB = new StringBuffer(str);
2.StringBuffer strB = new StringBuffer(); strB = strB.append(str) append)()会返回当前对象的指向

StringBuilder(可增删,效率最高,线程不安全)

与StringBuffer地API兼容,是它的一个简易替换,但线程不安全,在单线程情况下建议使用。

Date

有SimpleDateFormat类可以与其配套使用
SimpleDateFormat用于初始化的参数为日期的格式(字符串)日期时间的格式化代码
SimpleDateFormat中有方法format可以使日期时间按要求输出
parse方法可以用来将字符串转为日期格式(要与其自身格式相符合)(有异常需要处理)

Calendar

Calendar是一个抽象类,且其构造器是私有的,需要用getInstance来获取实例

LocalDateTime LocalDate LocalTime

LocalDateTime获取年月日时分秒 LocalDate获取年月日 LocalTime获取时分秒
DateTimeFormatter用于对它进行格式化
DateTimeFormatter dtf = DateTimeFormatter.ofPattern(“格式”)

泛型

泛型方法

规则 :
所有泛型方法都有一个类型参数声明部分,声明在方法返回类型之前
类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数占位符
类型参数只能代表引用类型
在指定类型参数后可以传入该类型或其子类型,默认情况下会给Object类型

注意事项

泛型类: class 类名 <>

  • 普通成员可以使用泛型(属性、方法)
  • 使用泛型的数组不能初始化(类型不确定,开辟的空间大小未知)
  • 静态方法中不能使用类的泛型(静态方法会跟着类的加载进行加载,而当时类的泛型尚未确定),但可以使用方法的泛型
  • 泛型类的类型,是在创建对象时确定的

泛型接口:interface 接口名 <>

  • 静态成员不能使用泛型
  • 泛型接口的类型参数,在继承接口实现接口时确定

泛型方法:<> 返回类型 函数名()

  • 可以定义在普通类或泛型类中
  • 泛型被调用时类型会被确定,即对应的类型参数会被绑定到对应类型,但调用结束后会取消绑定。

泛型类

规则:在类名后声明类型参数

类型通配符

一般使用?代替具体的类型参数
类型通配符上限:
<? extends class> 泛型参数只能是class子类.
类型通配符下限:
<? super class> 泛型参数只能是class父类.

Java集合

主要包含两种类型的容器:
1.Collection 存储一个元素集合 其下有两重要子集 List Set(Collection没有直接实现子类,而是通过子接口实现)
2.Map 存储键/值对映射

在集合中存储的均为对象的引用,因此若在插入集合后对元素进行修改可能会影响删除操作
例如在HashSet中,若修改与计算hash值有关的对象属性,则可能会导致删除不成功,因为修改前与修改后生成的hash值不同

Collection

Set和List

Set接口的存储是无序的,不重复的数据。List接口实例存储的是有序的,可重复的元素
Set检索效率低下,删除和插入效率高,插入和删除不会引起元素位置改变
List和数组类似,可以动态增长,根据实际存储的数据的长度自动增长,查找效率高,插入删除效率低。

ArrayList

ArrayList底层是用数组来实现的
ArrayList与Vector之间的区别就是ArrayList是线程不安全的。
ArrayList中维护了一个Object类型的数组 transient Object[] elementData

扩容机制

若使用无参构造器来创建,则初始化容量为0,第一个元素添加时,将容量扩展至10,此后如需再次扩容,则扩容为当前容量大小的1.5倍。
若使用指定容量的构造器,则之后扩容时,直接1.5倍。
扩容使用的是Arrays.copyOf()

Vector

Vector底层是 protected Object[] elementData
Vector线程安全
扩容机制:
如果是无参,默认10,满后按2倍扩。若有参,直接按2倍扩。

LinkedList

底层实现了双向链表和双端队列
可以添加任意元素
线程不安全

Java Iterator

iterator本身不存储数据,只用于遍历集合
迭代器可以类比为有头结点的链表
迭代器it三个基本操作:next, hasNext, remove
it.next() 返回下一个元素(Object),并且更新迭代器的状态
it.hasNext()用于检测集合中是否还有元素
it.remove() 将迭代器返回的元素删除 remove掉的是当前指向的元素,而非next元素

获取迭代器:集合想获取一个迭代器可以使用iterator()方法

增强for的底层就是迭代器。

HashSet

底层机制:
1.底层是HashMap 用邻接表实现他的装载因子默认是0.75
2.添加一个元素时,先得到hash值,然后转成索引值
3.找到存储数据的表table,检查当前位置是否已经存放了元素
4.如果没有数据就直接插入,
如果有数据,调用equals(调用的是当前对象的equals)比较,如果相同,则放弃添加,如果不相同就添加到最后。
5.若一条链表的元素个数超过TREEIFY_THRESHOLD(默认为8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认为64),就会进行树化(红黑树),若链表个数超限,而表空间充足,则优先将表扩容(每次容量都*2)。

add详细过程:
首先检查是否为空表,若为空表,则将其大小初始化为16
接着用插入对象的hashCode计算hash值(hashCode和hash值不一样)
然后用hash值计算得到对应的数据表中的索引
如果对应索引为空,则new一个Node直接插入
如果对应索引不为空:
(1)若插入位置已有对象的hash于插入对象的hash相同,且满足键值相同(即对象地址)或是调用当前对象的equals方法得到true则不插入
(2)若p为树节点,则调用红黑树的对应方法
(3)进入表中对应索引的链表,依次比较(方法同情况(1)相同)在插入完成后立即判断当前结点数是否达到8,若达到则进行树化(实际上在table表元素个数到64的情况下才会真的树化)

当结点数>0.75*容量时,扩容

LinkedHashSet

底层维护一个数组+双向链表
双向链表结点 Entry 内含元素:before,after,数据
表中元素为:head, tail, table, entrySet
双向链表使得LinkedHashSet具备插入顺序与遍历顺序相同的特征,每一次插入,首先会找到对应hash值的索引的首个结点,然后从该节点开始沿着双向链表一个个比较,因此相比HashSet效率偏低。总结:双向链表将原本分布在table各索引中的结点串成了一串。

HashMap

table:HashMap$Node[]
Map中的key和value可以是任何引用类型的数据,会封装到HashMap$Node(实现Entry接口)
Map中的key不允许重复(具体去重操作同HashSet),若插入重复key,则后插入的value会覆盖之前的,且为null的key只能有一个,且会将被覆盖的value返回。
Map中的value可以重复
Map会维护一个entrySet用于方便遍历Map中的数据,其中的Entry是Node的父类,并且提供getKey和getValue方法,存储的是表中数据的地址
HashMap线程不安全。

HashTable

key和value都不能为null
线程安全
使用方法同HashMap一样
底层:维护一个数组HashTable$Entry[] table 初始化大小为11
每次扩容将当前大小*2 + 1

Properties

继承了HashTable,且实现了Map接口

TreeSet(带Tree的都可以排序)

本质是二叉搜索树,其迭代器使用中序遍历
若用无参构造器创建TreeSet则其依旧是无序的,若要排序,需要传入一个Comparator对象
键值调用比较方法(键值不可为空)
TreeSet的去重机制,调用构造时传入的Comparator对象进行比较,若没有传入,则会调用插入对象实现的Comparator接口,若没有实现会报错,因为在底层会尝试将传入对象转变为Comparable对象

Collection工具类

reverse(Collection):反转集合中元素
shuffle(Collection):将元素随机排列
sort(Collection, [Comparator]):排序 默认从小到大
max(Collection, [Comparator])
min(Collection, [Comparator])
swap(Collection, int, int) :交换元素
frequency(Collection, Object):返回某元素出现次数
copy(Collection dest, Collection src):将src中的数据拷贝到dest中,前提是dest的大小不小于src的大小
replaceAll(List, Object oldVal, Object newVal)

绘图技术

1.先定义一个MyPanel类,继承JPanel类(MyPanel相当于画盘,其中paint方法的参数Graphics相当于画笔,带有很多画图方法)
2.让用于画图的类继承JFrame(一个框架,表示Graphics画图的界面)

绘图原理

Component类提供了两个和绘图相关最重要的方法
1.paint(Graphics g)绘制组件外观
2.repaint()刷新组件外观

当组件第一次在屏幕显示的时候,程序会自动地调用paint()方法
在以下情况,paint()也会被调用:

  • 窗口最小化,再最大化
  • 窗口的大小发生变化
  • repaint函数被调用

获取图片资源
Image image = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource(“图片所在路径(要放在项目的根目录下)”))

Java事件处理机制

java事件处理是采取“委派事件模型”。当事件发生时,产生事件的对象,会把此“信息”传递给“事件的监听者”处理,这里所说的“信息”实际上就是java.awt.event事件库里某个类所创建的对象。

线程

进程

进程是指运行中的程序,启动一个进程后,操作系统就会为该进程分配内存空间
进程是指程序的一次执行过程,或是正在运行的一个程序,是动态过程
只有进程中的所有线程全部死亡时,进程才会死亡

线程

线程是由进程创建的,是进程的一个实体。
一个进程可以拥有多个线程。

单线程:同一个时刻,只允许进行一个线程
多线程:同一个时刻,允许执行多个线程

并发:同一个时刻,多个任务交替进行, 单核CPU实现的多任务就是并发
并行:同一个时刻,多个任务同时执行,多核CPU可以实现并行

用户线程:当线程的任务执行完成或被通知结束时结束
守护线程:一般是为工作线程服务,当所有的用户线程结束时,守护线程自动结束
常见的守护线程:垃圾回收机制
将一个线程设置为守护线程:调用setDaemon()

JVM在所有非守护线程退出后退出

线程的礼让与插队

yield:线程礼让,让出CPU,让其他线程执行,但礼让的事件不确定,所以也不一定礼让成功(当系统认为CPU资源充足时不会礼让)
join:线程插队,插队的线程一旦插队成功,则肯定先执行完插入的线程所有任务

线程的生命周期

  • 新建状态:使用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新建状态,它保持这个状态直到程序start()这个线程
  • 就绪状态:当线程对象调用了start()方法之后,该线程就进入就绪状态,此时线程处于就绪队列中,等待JVM里线程调度器的调度 实现多线程的核心
  • 运行状态:就绪状态的线程获取CPU资源,就可以执行run(),此时它就处于运行状态,它可以变为阻塞状态,就绪状态和死亡状态 run()只是一个普通方法
  • 阻塞状态:
    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态

用Runnable接口创建线程

创建一个实现了Runnable接口的类,并实现run(),然后将其作为参数调用Thread的构造函数,用创建的thread类来调用start()
实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制。

线程常用方法

setName 设置线程名称
getName 返回线程名称
setPriority 更改线程优先级
getPriority 获取线程的优先级
interrupt 中断线程

线程同步

用synchronized修饰需要上锁的对象,使得该对象在任意时刻只能由一个线程访问
锁可以简单理解为一个标记对象,当该对象处于调用状态时代表别的线程此刻不能进行相关操作
同步方法/代码块(非静态的)的锁可以是this,也可以是其他属性对象(要求各线程能接触到的锁是同一个对象)
同步方法/代码块(静态的)的锁就是对象本身

释放锁

  • 当前线程的同步方法、同步代码块执行结束
  • 当前线程在同步代码块中遇到break,return
  • 当前线程在同步代码块,同步方法中出现了未处理的Error或Exception导致异常结束
  • 当前线程在同步代码块,同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁

注:当线程执行同步代码块或同步方法时,程序调用Thread.sleep()或yield()不会释放锁

并发编程

并发:同时完成多个任务,无需等待当前任务完成就可以执行其他的任务。解决了程序因为外部控制导致的阻塞,例如IO。因此并发问题常见于IO密集型任务。

并行:同时完成在多个位置,完成多个任务。即让多个CPU同时执行程序的不同部分来提升效率。

并发通过对共享资源的有效控制,提升程序效率。而并行则是通过使用更多的资源,来提升效率。

trick:抽象泄露,抽象可以通过屏蔽对任务不重要的部分,让人更加容易地理解并设计程序。但抽象如果有所遗漏,即使这些细节被隐藏,也难以掩盖它带来的影响。而支持并发的语言和库似乎多少都有这个问题。

并发的使用条件

并发操作需要CPU切换上下文,这会消耗CPU一定的性能。因此,如果程序是CPU密集的,即CPU一般都处于忙碌状态,此时使用并发是没有意义的,应当确保程序开启的线程数和CPU的核心数相等。

但如果CPU会因某些原因陷入阻塞状态,那么此时使用并发绝对是值得的。

Lock

公平锁:先来先服务,不考虑优先级。

非公平锁:可以根据优先级夺取资源。

可重入锁(递归锁)

当某一个线程已经获取了某个锁,那么再次获取该锁就不会被阻塞。最典型的就是在递归调用中,如果在上层调用已经获取了该锁,那么下层执行就不再需要去获取这个锁。

乐观锁:若得不到锁,就假设不会发生冲突,不加锁去完成某项操作,如果因为冲突导致失败就不断重试直至成功。

悲观锁:其他线程只能依靠阻塞来等待线程锁的释放。

lock简单用法

class Ticket {

    private int number = 0;
	// 默认构建非公平锁,若传入true则构建公平锁
    Lock lock = new ReentrantLock();
	
    public void sale() {
        // 注意不要在方法内构建锁,否则每次调用多使用不同的锁,毫无意义
        // lock还有一个boolean tryLock()方法,该方法会试探性地去获取锁,如果获取不到锁也不会等待,转而去执行之后的逻辑。
        lock.lock();
        // 上锁代码块用try-catch包裹,避免执行出现异常导致锁无法释放
        try {
            while (number < 50) {
                number++;
                System.out.println(Thread.currentThread().getName() + "卖出了第" + number + "张票");
                
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        } 
    }
}

synchronized 和 lock的区别

  1. 一个是关键字,一个是接口。
  2. synchronized会自动释放锁,而lock必须手动释放,因此同步代码块必须用try-catch包裹。
  3. synchronized无法中断等待,只能等待锁的释放,而lock可以中断等待,节省资源给其他可以执行的线程。
  4. Lock 可以通过 trylock 来知道有没有获取锁,而 synchronized 不能获取锁的状态。
  5. lock适合大量的代码同步问题,synchronized反之。

ReadWriteLock

读写锁,将一把锁分为读锁和写锁两个部分。读锁允许被多个线程获得,写锁只允许被单个线程获得。

特点:读读不互斥,读写互斥,写写互斥。

适用于读多写少的情况。

example:读者写者问题(写者优先)实现

读写锁用于确保读读不互斥,其余互斥,其余两个锁,一个用于确保读者数正确修改,一个用于确保当写者到来时,之后的读者都被阻塞。

class ReadAndWrite {

    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Lock lock = new ReentrantLock();
    private Lock writerPriorLock = new ReentrantLock();
    private int readCount = 0;

    public void read() throws InterruptedException {
        try {
            System.out.printf("new reader %s come\n", Thread.currentThread().getName());
            writerPriorLock.lock();
            readWriteLock.readLock().lock();
            lock.lock();
            readCount++;
            System.out.printf("reader %s start reading, readCount: %d\n", Thread.currentThread().getName(), readCount);
            lock.unlock();
            writerPriorLock.unlock();
            TimeUnit.SECONDS.sleep(3);
        } catch (Exception e) {
            //TODO: handle exception
        } finally {
            
            readWriteLock.readLock().unlock();
        }
        
    }

    public void write() {
        try {
            System.out.printf("new writer %s come\n", Thread.currentThread().getName());
            writerPriorLock.lock();
            readWriteLock.writeLock().lock();
            System.out.printf("writer %s start writing\n", Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(5);
            System.out.printf("writer %s finish writing\n", Thread.currentThread().getName());
        } catch (Exception e) {
            //TODO: handle exception    
        } finally {
            writerPriorLock.unlock();
            readWriteLock.writeLock().unlock();
        }
    }

}

生产者消费者

class Data {

    private int num = 0;

    public synchronized void increment() throws InterruptedException {
        // 循环校验,避免锁释放时,多个线程开始操作,即防止虚假唤醒。
        while (num != 0) {
            this.wait();
        }
        
        num++;
        System.out.println(Thread.currentThread().getName() + ": " + num);
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        while (num != 1) {
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName() + ": " + num);
        this.notifyAll();
    }

}

lock实现

Condition代表涉及PV操作的情况,通过对应的锁来获取。

await()相当于P操作,signal()相当于V操作

class Fruit {

    private Lock lock = new ReentrantLock();
    private Condition empty = lock.newCondition();
    private Condition apple = lock.newCondition();
    private Condition pear = lock.newCondition();
    private int num = 1;

    public void putApple() {
        try {
            lock.lock();
            while (num != 1) {
                empty.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName() + " put an apple");
            apple.signal();
        } catch (Exception e) {
            //TODO: handle exception
        } finally {
            lock.unlock();
        }
    }

    public void putPear() {
        try {
            lock.lock();
            while (num != 1) {
                empty.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName() + " put an pear");
            pear.signal();
        } catch (Exception e) {

        } finally {
            lock.unlock();
        }
    }

    public void eatApple() {
        try {
            lock.lock();
            while (num != 0) {
                apple.await();
            }
            num++;
            System.out.println(Thread.currentThread().getName() + " eat an apple");
            empty.signal();
        } catch (Exception e) {
            //TODO: handle exception
        } finally {
            lock.unlock();
        }
    }

    public void eatPear() {
        try {
            lock.lock();
            while (num != 0) {
                pear.await(); 
            }
            num++;
            System.out.println(Thread.currentThread().getName() + " eat a pear");
            empty.signal();
        } catch (Exception e) {

        } finally {
            lock.unlock();
        }
    }

}

COW

涉及java中集合的并发操作时,不能使用常见的ArrayList等,而要转去是使用ConcurrentXXX或是CopyOnWriteList这一类线程安全的集合类。在此简单介绍一下COW策略。

COW即写时复制,当有多个调用者同时请求一个资源的时候,这些调用者会同时获得一个指向相同资源的指针。当某个调用者要进行修改时,则会拷贝一份资源给调用者,在修改没有完成前,所有的操作都对其他调用者透明,直到操作完成并保存。

由于COW要求拷贝一份资源,因此会需要双倍内存(可以通过控制拷贝的范围来减少内存消耗),因此更适用于读多写少的情景,同时拷贝也会带来更新延迟的问题,但其性能比使用Vector要高。

异步通信

Callable<V>接口,要求实现call()方法,V代表返回值的类型。相比于Runable,该接口多了接受返回值的功能,但如果获取返回值的代码块执行时间较长,可能会导致程序在接受返回值的部分阻塞,因此需要谨慎设置获取返回值的位置。

FutureTask<V>,Runable的实现类,可以用于转换Callable和Runable,使得Callable的实现类能够被放入线程中执行。此外,FutureTask内部维护一个状态变量state,记录这个是否已经开始执行。当多个线程同时执行该任务时,只会有一个线程去真正执行这个任务,其他线程会发现state显示当前任务已经在执行或执行完,直接开始等待返回值,避免大量线程进行重复操作给服务器带来的性能损耗。

典型的使用场景就是,当缓存失效时,大量的查询涌入,但不应该让他们全部都去查询数据库。此时使用FutureTask就能大大减少数据库的查询次数。

example 避免重复点赞

Server.java

public class Server {
    
    private final Map<Integer, FutureTask<Boolean>> cache = new ConcurrentHashMap<>();

    public void thumb(int id) {
        // 如果没有重复任务,创建一个futuretask并执行
        FutureTask<Boolean> task = createTaskIfAbsent(id);
        try {
            System.out.println(task.get());
        } catch (Exception e) {
            //TODO: handle exception
        }
    }

    private FutureTask<Boolean> createTaskIfAbsent(int id) {
        
        // 尝试获取对应的任务
        FutureTask<Boolean> task = cache.get(id);
        // 如果当前没有执行相同的任务
        if (task == null) {
            // 创建一个点赞任务
            Callable<Boolean> thumb = new Thumb(id);
            task = new FutureTask<Boolean>(thumb);
            // 如果没有被其他线程抢先放入,则执行该任务
            FutureTask<Boolean> futureTask = cache.putIfAbsent(id, task);
            if (futureTask == null) {
                task.run();
            }
        }
        return task;
    }
}

Thumb.java

public class Thumb implements Callable<Boolean> {

    private int target;

    public Thumb(int target) {
        super();
        this.target = target;
    }

    @Override
    public Boolean call() throws Exception {
        System.out.println("Thumb: " + target);
        return true;
    }

}

forkjoin

采用分治思想,将一个大型问题拆分成无数个小问题,最后将结果合并。

实现并发排序

// RecursiveAction 执行的并发操作没有返回值
// RecursiveTask<T> 执行的并发操作有返回值,类型为T
public class SortTask extends RecursiveAction {

    private int start;
    private int end;
    // 插排与归并转换的临界值
    private Long limit = 16L;
    // 辅助排序的数组
    private Long[] nums;
    private Long[] tmp;


    public SortTask(int start, int end, Long[] nums) {
        this.start = start;
        this.end = end;
        this.nums = nums;
        this.tmp = new Long[nums.length];
    }
    
    
    private void insertSort(int start, int end) {
        for (int i = start; i < end; i++) {
            Long t = nums[i];
            int j = i;
            for (; j > start; j--) {
                if (nums[j - 1] > t) {
                    nums[j] = nums[j - 1];
                } else {
                    break;
                }
            }
            nums[j] = t;
        }
    }

    private void merge(int start, int mid, int end) {
        int i = start;
        int j = mid;
        int k = start;
        while (i < mid && j < end) {
            if (nums[i] < nums[j]) {
                tmp[k++] = nums[i++];
            } else {
                tmp[k++] = nums[j++];
            }
        }
        while (i < mid) {
            tmp[k++] = nums[i++];
        }
        while (j < end) {
            tmp[k++] = nums[j++];
        }
        for (int t = start; t < end; t++) {
            nums[t] = tmp[t];
        }
    }

    @Override
    protected void compute() {
        if ((end - start) < limit) {
            insertSort(start, end);
        } else {
            int mid = start + (end - start) / 2;
            SortTask task1 = new SortTask(start, mid, nums);
            SortTask task2 = new SortTask(mid, end, nums);
            
            invokeAll(task1, task2);
            task1.join();
            task2.join();
            merge(start, mid, end);
        }
    }

    public static void main(String[] args) {
        int length = 32;
        Long[] arr = new Long[length];
        Random random = new Random();
        Map<Long, Integer> map = new HashMap<>();
        for (int i = 0; i < length; i++) {
            Long num = random.nextLong();
            arr[i] = num;
            if (map.get(num) != null) {
                map.put(num, map.get(num) + 1);
            } else {
                map.put(num, 1);
            }
        }
        SortTask sortTask = new SortTask(0, length, arr);
        Long start = System.currentTimeMillis();
        sortTask.compute();
        Long end = System.currentTimeMillis();
        System.out.println(end - start);
        for (int i = 0; i < sortTask.nums.length - 1; i++) {
            // assert sortTask.res[i] < sortTask.res[i+1];
            if (sortTask.nums[i] > sortTask.nums[i+1]) {
                System.out.println("False");
                break;
            }
        }
        for (int i = 0; i < sortTask.nums.length; i++) {
            Long num = sortTask.nums[i];
            int count = map.get(num);
            if ((count - 1) == 0) {
                map.remove(num);
            } else {
                count--;
                map.put(num, count);
            }
        }
        if (!map.isEmpty()) {
            System.out.println("false");
        }
    }
}

计数器

countDownLatch

CountDownLatch,一个计数器,在计数器没有归零时,会阻塞指定的线程,等待所有工作完成。一般用于同步线程。例如工作C需要工作A,B的输入,则可以用计数器来判断当前状况是否允许执行C。Condition也可完成这类工作,但如果需要等待的对象过多,且不需要高度的精确性,使用计数器会更简单高效。

CountDownLatch count = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
    new Thread(() -> {
    System.out.println(Thread.currentThread().getName() + " finished");
    count.countDown();
	}, String.valueOf(i)).start();
}
count.await();
System.out.println("shutdown");

CyclicBarrier

一个同步辅助类,允许一组线程互相等待,直到达到某个公共屏障点。

其使用情景是,一组固定大小的线程,必须互相等待,而非特定的一些线程等待某些线程。

构造函数:

  1. CyclicBarrier(int parties),在达到指定数量的参与者时启动,并放行线程。
  2. CyclicBarrier(int parties, Runnable barrierAction),在达到指定数量的参与者时,执行预定义的操作barrierAction。
protected static void CyclicBarrierDemo() {
	CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    	System.out.println("ok");
    });
    for (int i = 0; i < 3; i++) {
    	final int num = i;
        new Thread(() -> {
        	System.out.println(num + "ok");
            try {
            	barrier.await();
            } catch (InterruptedException e) {
            	e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
       	}).start();
    }
}

CountDownLatch相比,CyclicBarrier发起等待的不是需求参数的线程,而是完成任务的线程,在指定数量的线程进入等待后,就执行预设的操作,并放行这些线程。除此之外,CyclicBarrier是可以重用的,即每次barrier被触发后就会重置为初始状态,等待下一轮线程的到达。而CountDownLatch在放行之后就一直处于开放状态。

信号量

semaphore代表信号量,可以执行PV操作。

acquire()为P操作,release()为V操作,初始化传入的参数为可用资源数。若资源数不足,acquire就会被阻塞,等待其他线程释放资源。

protected static void semaphoreDemo() {
    Semaphore semaphore = new Semaphore(3);
    for (int i = 0; i < 6; i++) {
    	final int num = i;
        new Thread(() -> {
        	try {
            	semaphore.acquire();
                System.out.println(num + "get the resource");
                TimeUnit.SECONDS.sleep(3);
                System.out.println(num + "release the resource");
            } catch (Exception e) {
                    
            } finally {
            	semaphore.release();
            }
        }).start();
	}
}

线程池

由于创建线程需要消耗系统资源,频繁的创建和销毁线程会占用大量的CPU时间,因此如果能复用线程,将会大大提高效率。

线程池就是在内部维护一组线程,在没有任务时都处于等待状态,任务到来时就将其分配给其中一个线程执行。

使用场景

在响应时间优先的情况下,使用线程池可以一次性地启动多个线程执行任务,达到提高响应速度的目的。并且此时会偏向与提高线程池的核心线程数大小以及最大线程数大小来尽可能创造更多的线程执行任务。阻塞队列则往往不会设置。

在吞吐量优先的情况下,此时不需要追求结果的快速返回,而是在有限的资源条件下,尽可能在单位时间完成更多的任务,此时往往需要设置队列来缓冲并发任务,线程数量也需要合理设置,避免过多线程导致大量的上下文切换,拉低吞吐量。

构造函数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                         RejectedExecutionHandler handler) 

corePoolSize:线程池核心线程数,即常驻线程数。线程池在初始化时没有线程,当任务到来时才开始创建线程。

maximumPoolSize:最大线程数,只有当阻塞队列满时,才会创建更多的线程

keepAliveTime:非核心线程的空闲时间超过该参数时,就会被回收

unit:超时的时间单位

workQueue:用于保存任务的队列

threadFactory:创建线程的工厂类。

handler:线程池无法继续接受任务时采取的拒绝策略。

拒绝策略

  • AbortPolicy:中断抛出异常
  • DiscardPolicy:丢弃任务,不进行任何通知
  • DiscardOldestPolicy:丢弃在队列中存在时间最久的任务
  • CallerRunsPolicy:让提交任务的线程去执行任务

函数式接口

接口中只有一个抽象方法的接口称为函数式接口。

和接口实现类相比,函数式接口可以直接传入静态方法而无需实例化,粒度更小,自由度更高。

四大函数式接口:

  1. Function<T, R>:T为入参类型,R为返回类型,一般函数的接口
  2. Predicate<T>:T为入参类型,返回为布尔类型,判断传入参数是否满足某个条件
  3. Consumer<T>:T为入参类型,没有返回值
  4. Supplier<T>:T为返回类型,没有入参

文件

文件在程序中是以流的形式来操作
流:数据在数据源(文件)和程序(内存)之间经历的路径
输入流:数据从数据源(文件)到程序(内存)的路径
输出流:数据从程序(内存)到数据源(文件)的路径

流的分类:
按操作数据单位:字节流(8bit)二进制文件,字符流(按字符)文本文件
按数据流的流向:输入流,输出流
按流的角色:节点流,处理流/包装流

字节流: InputStream OutputStream
字符流:Reader Writer

所有用到缓冲区的输出流每次写入完成后要刷新才能将数据写入

PS:在不需要流之后要将其关闭,避免资源浪费。

节点流和处理流

节点流:可以从一个特定的数据源读写数据
处理流:可以从任意数据源读写数据,并且自带缓冲 一般前缀为Buffered

处理流的构建:在构建时向其传入一个节点流,即可使其同时具备对应节点流与处理流带有的扩充功能。
关闭处理流:只需关闭外层流即可,内部的节点流会在close方法中被关闭
BufferedReader/BufferedWriter不适合处理二进制文件

System.in 表示标准输入 键盘
编译类型:InputStream
运行类型:BufferedInputStream

System.out 表示标准输出 显示器
编译类型:PrintStream
运行类型:PrintStream

转换流

可以将读取到的字节数据经过指定编码转换成字符 InputStreamReader
可以将读取到的字符数据经过指定编码转换成字节 OutputStreamWriter

应用场景:

  • 源或者目的对应的设备是字节流,但是操作的却是文本数据
  • 操作文本涉及到具体的指定编码表

打印流:PrintWriter, PrintStream(只有输出没有输入)

Java序列化

机制:
一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型
将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化(读取顺序必须和存储顺序一致)
类ObjectInputStream和ObjectOutputStream是高层次的数据流,他们包含反序列化和序列化对象的方法。

一个类的对象要想序列化成功,必须满足两个条件:
1.该类必须实现java.io.Serializable接口
2.该类的所有属性必须是可序列化的,如果有一个属性不是可序列化的,则该属性必须注明是短暂的。

注意事项:

  • 存储与读取的顺序必须一致
  • static或transient修饰的属性不会被序列化
  • 序列化对象时,要求内部的属性也实现序列化接口

序列化知识

反射

1.简介

  • 定义:能够分析类能力的程序称为反射
  • 作用:
    1.在运行时分析类的能力
    2.在运行时查看对象
    3.实现通用的数组操作代码
    4.利用Method对象
  • 价值:在不修改源码的情况下,增加、修改功能
  • 原理:JVM将类的字节码文件通过类加载器生成Class类对象存放在堆中(每个类只生成一个)

2.常用类

java.lang.reflect包中有三个类

  • Field 用于描述类的域

  • Method 用于描述类的方法

  • Constructor 用于描述类的构造器

    (具体内容详见java核心技术P194)

私有属性,方法,构造器,不可通过对应的getXXX()获得

2.1Class类

在程序加载完类之后,会调用ClassLoader创建该类的一个Class对象,并将其存储在堆中,其中保存了该类的所有信息。(每一个类只会创建一个Class对象)

同时会在方法区存储类的二进制数据

PS:一个Class对象实际上表示的是一个类型,而这个类型未必一定是一种类(也有可能是原始数据类型 ,eg:int.class就是一个Class类,但int不是一个类)

newInstance()可用于动态的创建一个类的实例,他调用默认的构造器初始化新创建的对象。

`eg:` 
`String s = "java.util.Random";`
`Object m = Class.forName(s).newInstance();`

调用有参构造器

Class cls = Class.getForName("Cat");
Constructor constructor = cls.getConstructor(String.class);
//获取构造器,参数为获取的构造器需要的参数的Class类
Object o1 = constructor.newInstance("Cat");

注意:在反射中都是用方法调用对象,若有返回值,则只会返回Object

method.invoke(obj)

反射调用优化:Field, Method, Constructor均有setAccessible(boolean)方法,设置为true可以关闭安全检查。(但会使得私有成员可以被访问)

2.2获取Class类

  1. 已知一个类的全类名,且该类在类路径下,可通过Class.forName()获取

    应用场景:多用于配置文件,读取类全路径,加载类

    String ClassFullPath = "com.test.Cat";
    Class cls = Class.forName(ClassFullPath);
  2. 已知具体的类,通过类的class获取,最安全可靠,且程序性能最高

    应用场景:参数传递

    class Test{}
    ...
    Class class = Test.class
  3. 已知实例,则可直接通过getClass()方法获得。

  4. 包装类可获取TYPE属性,它的哈希值与其对应的基本数据类型的Class的hash值相同.

3.类的加载

3.1静态,动态加载

  • 静态加载:编译时就加载相关类,依赖性较强
  • 动态记载:运行时加载需要的类

3.2类的加载时机

  • 创建对象时 //静态加载
  • 子类被加载时,父类也一并会被加载 //静态加载
  • 调用类中的静态成员时 //静态加载
  • 反射 //动态加载

3.3类加载的过程

  1. 加载:将类的class文件读入内存,并创建一个java.lang.Class对象

  2. 连接:将类的二进制数据合并到JRE中

    1. 验证:安全校验

      ​ 目的:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会威胁虚拟机自身安全

      ​ 包括:文件格式验证(是否以魔数oxcafebabe开头)、元数据验证、字节码验证和符号引用验证

      ​ PS:可以使用-Xverify:none参数来关闭大部分的类验证措施

    2. 准备:在该阶段对静态变量分配内存,并进行默认初始化(将其初始化为一个默认值,而非程序指定的值)

      ​ 而对于静态常量则会直接赋值

    3. 解析:JVM将常量池内的符号引用替换为直接引用的过程(可以理解为从图纸到实物的过程)

  3. 初始化:JVM负责对类进行初始化,主要是静态成员

    ​ 真正开始执行类中定义的Java程序代码,此阶段是执行<clinit>()方法的过程

    ​ 该方法由编译器按语句在源文件中出现的顺序,依次收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并。(该方法有线程锁)

类加载后内存布局:

方法区中存储类的二进制数据,并根据它在中生成一个类的Class对象

3.4通过反射创建对象

相关方法:

  • Class:
    • getConstructor(Class<?>) 获取对应参数列表的public构造器
    • getDeclaredConstructor(Class<?>)获取对应参数列表的任意构造器
  • Constructor:
    • newInstance()创建一个实例
    • setAccessible()暴破(暴破后可操作私有属性)

JDBC

JDBC:提供一套统一的数据库接口由数据库厂商实现

1.JDBC编写流程:

1.1注册驱动:获取Driver类
//com.mysql.cj.jdbc.Driver
Driver driver = new Driver();
1.2连接:获取Connection类
//url jdbc:mysql://ip地址:端口号/数据库名称?serverTimezone=UTC"
String url = "jdbc:mysql://localhost:3306/coursecrash?serverTimezone=UTC";
//登录信息
Properties properties = new Properties();
properties.setProperty("user", "root");
properties.setProperty("password", "Cxc020603");

Connection connect = driver.connect(url, properties);
1.3CRUD:发送SQL语句
String sql = "insert into ade values('lgd', 'shen')";

Statement statement = connect.createStatement();
int row = statement.executeUpdate(sql);
System.out.println(row);
1.4释放资源

2.数据库连接方式

  • 注册进DriverManage,方便统一管理
String url= "jdbc:mysql://localhost:3306/coursecrash?serverTimezone=UTC";
String user = "root";
String passwd = "Cxc020603";
Class<?> aClass = Class.forName("com.mysql.jdbc.Driver");
String sql = "insert into ade values('lgd', 'shen')";
Constructor<?> constructor = aClass.getConstructor();
Driver driver1 = (Driver) constructor.newInstance();
DriverManager.registerDriver(driver1); //注册驱动
Connection connection = DriverManager.getConnection(url, user, passwd);
//Statement用于执行静态的SQL语句,并返回结果
Statement statement = connection.createStatement();
statement.executeUpdate(sql); //返回值为影响的行数
statement.close();
  • 在类加载的时候(Class.forName())会自动注册数据库驱动,因此可以省略

  • mysql驱动5.1.6以上可以无需Class.forName(),从jdk1.5以后使用了jdbc4,不再需要显式调用class.forName()注册驱动而是自动调用驱动jar包下META-INF\services\java.sql.Driver文本中的类名称去注册

  • 通过配置文件获取连接的相关信息(软编码,推荐)

public static void main(String[] args) throws ClassNotFoundException, SQLException, IOException {

    Properties properties = new Properties();
    properties.load(new FileInputStream("src\\load.properties"));
    String user = properties.getProperty("user");
    String password = properties.getProperty("password");
    String url = properties.getProperty("url");
    String driver = properties.getProperty("driver");
    System.out.println(driver);
    

    Class.forName(driver); //加载驱动类

    Connection connection = DriverManager.getConnection(url, user, password);
    Statement statement = connection.createStatement();
    //String sql = "create table TestTable (name varchar(5), age int, id int AUTO_INCREMENT, primary key(id))";
    String sql = "insert into testtable values ('cxc', 18, null)," +
            "('pcx', 18, null), ('lgd', 20, null)";
    String sql1 = "delete from testtable where id=3";
    statement.executeUpdate(sql);
    statement.executeUpdate(sql1);
    statement.close();

}
url=jdbc:mysql://localhost:3306/coursecrash?serverTimezone=UTC
user=root
password=Cxc020603
driver=com.mysql.jdbc.Driver

ps:properties以字符串键值对存储,不需要加双引号

3.ResultSet

用exectueQuery(sql)处理传入的select语句后会返回一个结果集

ResultSet resultSet = statement.executeQuery(sql2);

//previous()向上移动一行
//相当于向下移动一行,初始状态下处于无效位置
while(resultSet.next()) 
{
    for(int i = 1; i <= 3; i++) {
        //取出指定列的数据
        System.out.print(resultSet.getXXX(i)); 
        //XXX指该列的数据类型,//传入的参数可以是列的索引,也可以是列名
    }
    System.out.println();
}

ResultSet中有一个rows属性,其数据类型为ArrayList,其中存储对象类数组,用于表示每一个数据

ResultSet与对应的连接绑定,一旦连接关闭,将无法再使用结果集

拓展:SQL注入

Statement存在SQL注入风险

SQL注入:利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的SQL语句段或命令,恶意攻击数据库

eg:

public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {

    Scanner scanner = new Scanner(System.in);
    Properties properties = new Properties();
    properties.load(new FileInputStream("src\\load.properties"));
    String user = properties.getProperty("user");
    String driver = properties.getProperty("driver");
    String password = properties.getProperty("password");
    String url = properties.getProperty("url");

    Class.forName(driver);
    Connection connection = DriverManager.getConnection(url, user, password);
    Statement statement = connection.createStatement();

    System.out.println("输入用户名:");
    String user1 = scanner.nextLine();
    System.out.println("输入密码");
    String passwd = scanner.nextLine();

    String sql = "select * from admin where name = '" + user1 + "' and password = '" + passwd + "'";
    System.out.println(sql);
    ResultSet resultSet = statement.executeQuery(sql);

    if(resultSet.next())
    {
        String sucess = "登陆成功";
        System.out.println(sucess);
    }
    else
    {
        String fail = "Fail";
        System.out.println(fail);
    }
}

运行结果

输入用户名:
1'
输入密码
or '1'='1
select * from admin where name = '1'' and password = 'or '1'='1'
登陆成功

Process finished with exit code 0

4.PreparedStatement

预处理Statement

预编译:在调用prepareStatement()时会直接将SQL语句提交给数据库编译,得到的PreparedStatement句柄是一个预编译好的SQL语句,添加参数后可直接执行。

将自身与某一sql语句绑定,sql语句中的参数用“?”作为占位符(占位符两边会自动加上引号)

因此占位符只能占位SQL语句中的普通值,绝不能占位表名,SQL关键字

调用setXXX(占位符的索引,占位符的值)方法来填充参数(参数索引从1开始)

String sql = "select * from admin where name = ? and password = ?;";
Connection connection = DriverManager.getConnection(url, user, password);
//此处将上方的sql语句与PreparedStatement绑定
PreparedStatement preparedStatement = connection.prepareStatement(sql);

System.out.println("输入用户名:");
String user1 = scanner.nextLine();
System.out.println("输入密码");
String passwd = scanner.nextLine();
preparedStatement.setString(1,user1);
preparedStatement.setString(2,passwd);
ResultSet resultSet = preparedStatement.executeQuery();
if(resultSet.next())
{
	System.out.println("success");
}
else
	System.out.println("failure");

优势:

  • 不通过字符串拼接写SQL语句
  • 可以防止SQL注入(自动在占位符两边添加引号)

5.JDBC事务处理

开启事务: 调用Connect对象的setCommit(false)方法

默认情况下自动提交

提交事务:调用Connect对象的commit()方法

回滚:调用Connect对象的rollback([savepoint])方法

6.批处理

通常与PreparedStatement搭配使用

在连接MySQL的url中添加参数rewriteBatchedStatements=true

addBatch()将当前的sql语句添加到批处理包中

executeBatch()将包中的sql语句全部执行一遍

clearBatch()清空包

Connection connect = JDBCUtil.connect();
PreparedStatement preparedStatement = null;

//String sql = "insert into ade values (?, ?)";
String sql = "delete from ade where name=?";
try {
    preparedStatement = connect.prepareStatement(sql);
    long start = System.currentTimeMillis();
    for(int i = 0; i < 1000; i++)
    {
        preparedStatement.setString(1, "cxc" + i);
        //preparedStatement.setString(2, "ohh");
        preparedStatement.addBatch();
    }
    preparedStatement.executeBatch();
    preparedStatement.clearBatch();
    long end = System.currentTimeMillis();
    System.out.println(end - start);
} catch (SQLException e) {
    e.printStackTrace();
}

作用:批量处理数据,减少编译次数,以及发送sql语句的网络开销

7.数据库连接池

7.1传统的获取连接的方式的问题分析

  1. 每次向数据库建立连接都需要将Connection加载到内存中,再验证IP地址,用户名和密码,需要数据库连接时就申请一个,频繁连接会占用大量资源,造成服务器崩溃
  2. 每次连接使用完都得断开,若程序出现异常未能关闭,会导致数据库内存泄漏
  3. 不能控制创建的连接数量,若连接过多,也有可能导致内存泄漏,MySQL崩溃(没有缓冲机制)

7.2连接池基本介绍

  1. 预先向缓冲池中加入一定数量的连接(已经创建好的)
  2. 数据库连接池负责分配,管理和释放数据库连接,他允许应用程序重复使用一个现有的数据库连接。每次程序使用完成之后,只是连接的引用消失,但连接本身依旧存在
  3. 当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列

7.3 c3p0连接池

  1. 导入jar包

连接方式1:

public static void main(String[] args) throws IOException, SQLException {

    ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
    Properties properties = new Properties();
    properties.load(new FileInputStream("src\\load.properties"));
    String user = properties.getProperty("user");
    String password = properties.getProperty("password");
    String driver = properties.getProperty("driver");
    String url = properties.getProperty("url");

    try {
        comboPooledDataSource.setDriverClass(driver);
        comboPooledDataSource.setJdbcUrl(url);
        comboPooledDataSource.setUser(user);
        comboPooledDataSource.setPassword(password);
    } catch (PropertyVetoException e) {
        e.printStackTrace();
    }

    comboPooledDataSource.setInitialPoolSize(10);
    comboPooledDataSource.setMaxPoolSize(500);

    long start = System.currentTimeMillis();
    for(int i = 0; i < 5000; i++)
    {
        Connection connection = comboPooledDataSource.getConnection();
        connection.close();
    }
    long end = System.currentTimeMillis();
    System.out.println(end - start);
}

连接方式2(配置文件):

<!-- 文件名 c3p0-config.xml 放置在src文件夹下 -->
<c3p0-config>
    <!-- 数据源名称 -->
    <named-config name="mySource">
    <!-- 若为default-config 则在生成管理对象时不需要传入参数 -->
        <!-- 驱动类 -->
        <property name="driverClass">com.mysql.cj.jdbc.Driver</property>
        <!-- url -->
        <property name="jdbcUrl">jdbc:mysql://localhost:3306/coursecrash?serverTimezone=UTC</property>
        <!-- user -->
        <property name="user">root</property>
        <!-- password -->
        <property name="password">Cxc020603</property>

        <!-- 初始化连接池中的连接数量大小 -->
        <property name="initialPoolSize">10</property>
        <!-- 最大连接时长 -->
        <property name="maxIdleTime">30</property>
        <!-- 最大连接数 -->
        <property name="maxPoolSize">100</property>
        <!-- 最小连接数 -->
        <property name="minPoolSize">10</property>
    </named-config>
</c3p0-config>
void connect2() throws SQLException {
    ComboPooledDataSource mySource = new ComboPooledDataSource("mySource");
    Connection connection = mySource.getConnection();
    connection.close();

}

7.4德鲁伊连接池

导入jar包

配置文件:

# 数据库连接参数
url=jdbc:mysql://localhost:3306/coursecrash?serverTimezone=UTC&rewriteBatchedStatements=true
username=root
password=Cxc020603
# 驱动
driverClassName=com.mysql.cj.jdbc.Driver
# 连接池的参数
# 初始虎啊连接数
initialSize=10
#最大连接数
maxActive=10
#最大等待时长
maxWait=200
void druid() throws Exception {
    Properties properties = new Properties();
    properties.load(new FileReader("src\\druid.properties"));
    DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
    Connection connection = dataSource.getConnection();
    connection.close();
}

8. ApDBUtils

8.1 POJO(简单java对象)

创建一个java对象,用其中的属性,映射ResultSet中的各个字段

8.2 DBUtils类

  1. QueryRunner类:该类封装了SQL的执行方法,线程安全,可以实现增删改查
  2. ResultSetHandler接口:该接口用于处理java.sql.ResultSet,将数据按要求转换为另一种形式(POJO)

常用ResultSet接口实现类:

  • ArrayHandler:把结果集中的每一行数据都转成一个数组,再存放到List中
  • BeanHandler:将结果集中的第一行数据封装到一个POJO中
  • BeanListHandler:将结果集中的每一行数据都封装到一个对应的POJO实例中
  • ScalarHandler:若结果中只有单个值,则用该实现类处理,若有多个值,则会返回第一列数据处理后结果。

8.3基本使用

public static void main(String[] args) throws SQLException {

    //获取连接
    Connection connection = DruidUtils.getConnection();
    //生成QueryRunner对象
    QueryRunner queryRunner = new QueryRunner();
    //需要预编译处理的语句
    String sql = "select * from ade where name=?";
    //query方法参数,连接,sql语句,ResultSetHandler接口,sql语句中的参数
    List<POJO> query =
            queryRunner.query(connection, sql, new BeanListHandler<>(POJO.class), "cxc");
    for (POJO pojo : query) {
        System.out.println(pojo);
    }
    DruidUtils.close(null, null, connection);
}

query底层使用preparedStatement操作sql语句,并会自动关闭Statement与ResultSet,在转换ResultSet中的数据时,会通过反射获取POJO类的属性,构造器等,然后返回一个对应的数组形式的结果集合。

若要传入基础数据类型,则要使用对应的包装类, 基本数据类型不能为空

9.BasicDao

DAO:data access object数据访问对象

此类对象专门用于对特定类的数据进行增删改查

具体关系:

XXPOJO映射XX表中的数据

XXDAO对XX表进行CRUD

10.Dao, Service, Controller

Dao:数据访问层,对数据进行增删改查

Service:业务逻辑层,通过Dao的组合实现具体的业务逻辑

Controller:控制层,转发Service的业务处理结果,但不暴露Service层的业务逻辑

  • 标题: Java
  • 作者: Zephyr
  • 创建于 : 2022-06-29 23:46:29
  • 更新于 : 2023-02-27 11:35:01
  • 链接: https://faustpromaxpx.github.io/2022/06/29/Java/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论