【前端】JavaScript

JavaScript

JavaScript是一门世界上最流行的脚本语言。ECMAScript可以理解为是JavaScript的一个标准

引入 JavaScript

  1. 内部标签
1
2
3
4
<!--Script标签内,写JavaScript代码-->
<script>
....
</script>
  1. 外部引入
1
<script src="js/helloworld.js"></script>

基本语法

1
2
3
4
5
6
7
8
9
10
11
12
<!--Script标签内,写JavaScript代码-->
<script>
// alert('hello world');
var score = 1;
if (score > 60){
alert(true);
} else {
alert(false);
}
// console.log() 在浏览器的控制台打印变量

</script>

严格检查格式

'use strict'; 严格检查模式,必须写在JavaScript第一行,防止JavaScript不严谨的语法误用。(前提:IDEA需要设置支持ES6语法)

阅读全文

【Java】单元测试

JUnit5 的变化

Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库。作为最新版本的JUnit框架,JUnit5与之前版本的JUnit框架有很大的不同。由三个不同子项目的几个不同模块组成:

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform: JUnit Platform是在JVM上启动测试框架的基础,不仅支持JUnit自制的测试引擎,其他测试引擎也都可以接入。
  • JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于在JUnit Platform上运行。
  • JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x和JUnit3.x的测试引擎。

image-20210813193410660

注意:Spring Boot 2.4 以上版本移除了默认对Vintage的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test)

JUnit 5’s Vintage Engine Removed from spring-boot-starter-test,如果需要继续兼容junit4需要自行引入vintage:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>

img

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

现在版本使用 @SpringBootTest

1
2
3
4
5
6
@SpringBootTest
class Boot05WebAdminApplicationTests {
@Test
void contextLoads() {
}
}

以前版本使用 @SpringBootTest + @RunWith(SpringTest.class)

Spring Boot整合JUnit以后:

  • 编写测试方法:@Test标注(注意需要使用JUnit5版本的注解)
  • JUnit类具有Spring的功能,@Autowired、比如 @Transactional 标注测试方法,测试完成后自动回滚

JUnit5常用注解

JUnit5的注解与JUnit4的注解有所变化:https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations

  • @Test 表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
  • @ParameterizedTest 表示方法是参数化测试,下方会有详细介绍
  • @RepeatedTest 表示方法可重复执行,下方会有详细介绍
  • @DisplayName 为测试类或者测试方法设置展示名称
  • @BeforeEach 表示在每个单元测试之前执行
  • @AfterEach 表示在每个单元测试之后执行
  • @BeforeAll 表示在所有单元测试之前执行
  • @AfterAll 表示在所有单元测试之后执行
  • @Tag 表示单元测试类别,类似于JUnit4中的 @Categories
  • @Disabled 表示测试类或测试方法不执行,类似于JUnit4中的 @Ignore
  • @Timeout 表示测试方法运行如果超过了指定时间将会返回错误
  • @ExtendWith 为测试类或测试方法提供扩展类引用
1
2
3
4
5
6
7
8
9
import org.junit.jupiter.api.Test; //注意这里使用的是JUnit5里jupiter的Test注解!!

public class TestDemo {
@Test
@DisplayName("第一次测试")
public void firstTest() {
System.out.println("hello world");
}
}

断言(Assertions)

断言(Assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法

断言用于检查业务逻辑返回的数据是否合理。所有的测试运行结束以后,会有一个详细的测试报告

简单断言

用来对单个值进行简单的验证。如:

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 true
assertFalse 判断给定的布尔值是否为 false
assertNull 判断给定的对象引用是否为 null
assertNotNull 判断给定的对象引用是否不为 null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
@DisplayName("simple assertion")
public void simple() {
assertEquals(3, 1 + 2, "simple math");
assertNotEquals(3, 1 + 1);

assertNotSame(new Object(), new Object());
Object obj = new Object();
assertSame(obj, obj);

assertFalse(1 > 2);
assertTrue(1 < 2);

assertNull(null);
assertNotNull(new Object());
}

数组断言

通过 assertArrayEquals() 方法来判断两个对象或原始类型的数组是否相等

1
2
3
4
5
@Test
@DisplayName("array assertion")
public void array() {
assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}

组合断言

assertAll() 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言

1
2
3
4
5
6
7
8
@Test
@DisplayName("assert all")
public void all() {
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0)
);
}

异常断言

在JUnit4时期,想要测试方法的异常情况时,需要用**@Rule**注解的ExpectedException变量,还是比较麻烦的。而JUnit5提供了一种新的断言方式 Assertions.assertThrows() ,配合函数式编程就可以进行使用。

1
2
3
4
5
6
7
@Test
@DisplayName("异常测试")
public void exceptionTest() {
ArithmeticException exception = Assertions.assertThrows(
//扔出断言异常
ArithmeticException.class, () -> System.out.println(1 % 0));
}

超时断言

JUnit5还提供了 Assertions.assertTimeout() 为测试方法设置了超时时间

1
2
3
4
5
6
@Test
@DisplayName("超时测试")
public void timeoutTest() {
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}

快速失败

通过 fail() 方法直接使得测试失败

1
2
3
4
5
@Test
@DisplayName("fail")
public void shouldFail() {
fail("This should fail");
}

前置条件(Assumptions)

JUnit 5 中的前置条件(Assumptions【假设】)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@DisplayName("前置条件")
public class AssumptionsTest {
private final String environment = "DEV";

@Test
@DisplayName("simple")
public void simpleAssume() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "PROD"));
}

@Test
@DisplayName("assume then do")
public void assumeThenDo() {
assumingThat(
Objects.equals(this.environment, "DEV"),
() -> System.out.println("In DEV")
);
}
}

assumeTrue()assumFalse() 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止(不会失败)

assumingThat() 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。

嵌套测试

JUnit 5 可以通过 Java 中的内部类和 @Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用 @BeforeEach@AfterEach 注解,而且嵌套的层次没有限制。

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
@DisplayName("A stack")
class TestingAStackDemo {

Stack<Object> stack;

@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}

@Nested
@DisplayName("when new")
class WhenNew {

@BeforeEach
void createNewStack() {
stack = new Stack<>();
}

@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}

@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}

@Nested
@DisplayName("after pushing an element")
class AfterPushing {

String anElement = "an element";

@BeforeEach
void pushAnElement() {
stack.push(anElement);
}

@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}

@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}

参数化测试

参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。

利用 @ValueSource 等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。

  • @ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
  • @NullSource: 表示为参数化测试提供一个null的入参
  • @EnumSource: 表示为参数化测试提供一个枚举入参
  • @CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
  • @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试1")
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}

@ParameterizedTest
@MethodSource("method") //指定方法名
@DisplayName("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}

static Stream<String> method() {
return Stream.of("apple", "banana");
}

迁移指南

在进行迁移的时候需要注意如下变化:

  • 注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中。
  • @Before@After 替换成 @BeforeEach@AfterEach
  • @BeforeClass@AfterClass 替换成 @BeforeAll@AfterAll
  • @Ignore 替换成 @Disabled
  • @Category 替换成 @Tag
  • @RunWith@Rule@ClassRule 替换成 @ExtendWith

【前端】HTML5 + CSS3

HTML

HTML

Hyper Text Markup Language(超文本标记语言)

W3C标准

W3C:World Wide Web Consortium(万维网联盟),成立于1994年,是Web技术领域最权威和最具影响力的国际中立性技术标准结构

W3C标准包括:

  • 结构化标准语言(HTML、XML)
  • 表现标准语言(CSS)
  • 行为标准(DOM、ECMAScript)
阅读全文

【MySQL】MySQL 事务

image-20210913132511709

事务原则

TCL: Transaction Control Language 事务控制语言。

事务(Transaction):事务由单独单元的一个或一组sql语句组成,在这个单元中,每个MySQL语句是相互依赖的,这个执行单元 要么全部执行,要么全部不执行

  • SQL语句1正在执行:A给B转账200 A:1000 ——> 200 B:200
  • SQL语句2正在执行:B收到A的钱 B:800 ——> B:400

将一组SQL放在一个批次中去执行。上述两条语句组成一组来执行,要么都成功,要么都失败,否则钱会凭空消失。

事务原则:ACID原则——原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability);脏读,幻读,不可重复读

1. 原子性(Atomicity)

原子性是指事务是一个不可分割的工作单位,事务中的操作要么都执行,要么都不执行,没有中间状态。对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。

2. 一致性(Consistency)

事务必须使数据库从一个一致性状态变换到另一个一致性状态。保证数据库一致性是指当事务完成时,必须使所有数据都具有一致的状态。(例如A给B转账前后,数据库中二者余额之和相等,转账前为一个一致性状态,转账后也为一个一致性状态)。

3. 隔离性(Isolation)

隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他数据是隔离的,并发执行的各个事务之间不能互相干扰。

4. 持久性(Durability)

持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。

其中,原子性和持久性靠undo和redo日志来实现,隔离性通过线程的控制来实现

事务隔离级别

参考链接:https://cloud.tencent.com/developer/article/1450773

对于同时运行的多个事务,当这些事务访问数据库中相同的数据时,如果没有采取必要的隔离机制,就会导致各种并发问题(按照严重性从高到低排序):

1. 脏写(修改未提交数据)

指一个事务修改了另一个事务未提交的数据。例如,两个事务T1和T2,T1修改了已经被T2更新但是还没有被提交的字段,并且提交了自己的事务,但之后,若T2回滚,则T1刚才修改的内容就是临时且无效的。

2. 脏读(读取未提交数据):

指一个事务读取了另一个事务未提交的数据。例如,两个事务T1和T2,T1读取了已经被T2更新但是还没有被提交的字段,之后,若T2回滚,T1读取到的内容就是临时且无效的。

3. 不可重复读(前后多次读取,数据内容不一致):

指在一个事务内读取表中的某一行数据,过段时间,该字段数据被另一事务修改,此时第一个事务再读时读取结果不同。例如,两个事务T1和T2,T1读取了一个字段,然后T2更新了该字段,之后,T1再此读取同一字段时,值就不同了。

4. 幻读(前后多次读取,数据总量不一致):

事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据。(类似班级里刚才看还是一个人,再看变成两个人,就像产生了幻觉)


不可重复读和幻读的区别:

(1) 不可重复读是读取了其他事务更改的数据,针对UPDATE操作

解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。

(2) 幻读是读取了其他事务新增的数据,针对INSERT操作

解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。

MySQL的JDBC中,默认开启事务,此时每一句sql语句都会在一个单独的事务中执行,例如两次查询语句都会在不同的事务中执行,执行完该语句都会立刻提交。


MySQL支持4种事务隔离级别,默认级别为 REPEATABLE READ

  • READ UNCOMMITTED(读未提交数据):允许事务读取未被其他事务提交的变更。脏读、不可重复读和幻读都可能出现
  • READ COMMITTED(读已提交数据):只允许事务读取已经被其他事务提交的变更。可以避免脏读,但不可重复读和幻读仍然可能出现
  • REPEATABLE READ(可重复读):确保事务可以多次从一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新。此时即使其他事务修改某字段并COMMIT,本事务查询时仍是原先值。可以避免脏读和不可重复读,但幻读仍然可能出现,即其他事务若插入了新的行,本事务查询时也会多出这些行,导致看起来像幻觉一样,每次读取的数据总量不同(注意,和一些其他数据库实现不同的是,可以简单认为 MySQL 在可重复读级别不会出现幻象读,因为其提供了 MVCC 与间隙锁的机制)
  • SERIALIZABLE(串行化):确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表执行插入,更新和删除操作。所有并发问题都可以避免,但性能十分低下。在某个事务读取时,其他事务阻塞,无法对该表进行操作。该级别下无法进行并发。本质上是添加了行级锁。并发事务之间是串行化的,通常意味着读取需要获取共享读锁,更新需要获取排他写锁,如果 SQL 使用 WHERE 语句,还会获取区间锁(MySQL 以间隙锁形式实现,可重复读级别中默认也会使用),这是最高的隔离级别。

四种隔离级别都解决了脏写问题,因为脏写问题是最严重的,会导致自己修改的数据被别人覆盖,因此必须解决

查看隔离级别:

1
2
SELECT @@TX_ISOLATION           --(8.0以前) 
SELECT @@TRANSACTION_ISOLATION --(8.0以后)

设置当前MySQL连接的隔离级别:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED

设置全局MySQL连接的隔离级别:

SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED

undo 和 redo 日志

http://www.zhdba.com/mysqlops/2012/04/06/innodb-log1/

在数据库系统中,既有存放数据的文件,也有存放日志的文件。在内存中既有日志缓存 log buffer,也有磁盘文件 log file。MySQL中的日志文件,有这么两种与事务有关:undo日志redo日志

undo 日志

https://www.bilibili.com/video/BV1n7411R7WQ?p=6&spm_id_from=333.1007.top_right_bar_window_history.content.click

undo日志的原理:为了满足事务的原子性,在操作任何数据之前,首先将数据备份到 undo log。然后进行数据的修改。如果出现了错误或者用户执行了 ROLLBACK 语句,系统可以利用 undo log 中的备份将数据恢复到事务开始之前的状态。并且数据库写入数据到磁盘之前,会把数据先缓存在内存中,事务提交时才会写入磁盘中。

用undo日志实现原子性的简化过程

假设有A、B两个数据,值分别为1,2。

  • A. 事务开始.
  • B. 记录A=1到 undo log.
  • C. 修改A=3.
  • D. 记录B=2到 undo log.
  • E. 修改B=4.
  • F. 将 undo log 写到磁盘。
  • G. 将数据写到磁盘。
  • H. 事务提交

1. 如何保证持久性?

事务提交前,会把修改后的数据保存到磁盘,也就是说只要事务提交了,数据肯定持久化了。

2. 如何保证原子性?

  • 每次对数据库修改,都会把修改前数据记录在 undo log,那么需要回滚时,可以读取 undo log,恢复数据。
  • 若系统在G和H之间崩溃,此时事务并未提交,需要回滚。而 undo log 已经被持久化,可以根据 undo log 来恢复数据
  • 若系统在G之前崩溃,此时数据并未持久化到硬盘,依然保持在事务之前的状态

缺陷:每个事务提交前将数据和 undo log 写入磁盘,这样会导致大量的磁盘IO,因此性能很低。

如果能够将数据缓存一段时间,就能减少IO提高性能。但是这样就会丧失事务的持久性。因此引入了另外一种机制来实现持久化,即 redo log

redo日志

和 undo log 相反,redo log 记录的是新数据的备份。在事务提交前,只要将 redo log 持久化即可,不需要将数据库中的数据持久化,减少了IO的次数。

undo + redo 事务的简化过程

假设有A、B两个数据,值分别为1,2

  • A. 事务开始.
  • B. 记录 A=1 到 undo log buffer.
  • C. 修改 A=3.
  • D. 记录 A=3 到 redo log buffer.
  • E. 记录 B=2 到 undo log buffer
  • F. 修改 B=4.
  • G. 记录 B=4 到 redo log buffer.
  • H. 将 undo log 写入磁盘
  • I. 将 redo log 写入磁盘
  • J. 事务提交

1. 如何保证原子性?

如果在事务提交前故障,通过 undo log 日志恢复数据。如果 undo log 都还没写入,那么数据就尚未持久化,无需回滚

2. 如何保证持久化?

注意,这里并没有出现数据的持久化。因为数据已经写入 redo log,而 redo log 持久化到了硬盘,因此只要到了步骤I以后,事务是可以提交的。

3. 内存中的数据库数据何时持久化到磁盘?

因为 redo log 已经持久化,因此数据库数据写入磁盘与否影响不大,不过为了避免出现脏数据(内存中与磁盘不一致),事务提交后也会将内存数据刷入磁盘(也可以按照固设定的频率刷新内存数据到磁盘中)。

4. redo log 何时写入磁盘?

redo log 会在事务提交之前,或者 redo log buffer满了的时候写入磁盘

这里存在两个问题:

问题1:之前是写 undo 和数据库数据到硬盘,现在是写 undo 和 redo 到磁盘,似乎没有减少IO次数

  • 数据库数据写入是随机IO,性能很差
  • redo log 在初始化时会开辟一段连续的空间,写入是顺序IO,性能很好
  • 实际上 undo log 并不是直接写入磁盘,而是先写入到 redo log buffer 中,当 redo log 持久化时,undo log 就同时持久化到硬盘了。

因此事务提交前,只需要对 redo log 持久化即可。另外,redo log 并不是写入一次就持久化一次, redo log 在内存中也有自己的缓冲池:redo log buffer。每次写 redo log 都是写入到 buffer,在提交时一次性持久化到磁盘,减少IO次数。

问题2:redo log 数据是写入内存buffer中,当buffer满或者事务提交时,将 buffer 数据写入磁盘。redo log 中记录的数据,有可能包含尚未提交事务,如果此时数据库崩溃,那么如何完成数据恢复?

数据恢复有两种策略:

  • 恢复时,只重做已经提交了的事务
  • 恢复时,重做所有事务包括未提交的事务和回滚了的事务。然后通过 undo log 回滚那些未提交的事务

Innodb 引擎采用的是第二种方案,因此 undo log 要在 redo log 前持久化

总结

  • undo log 记录更新前的数据,用于保证事务原子性
  • redo log 记录更新后的数据,用于保证事务的持久性
  • redo log 有自己的内存buffer,先写入到 buffer,事务提交时写入磁盘
  • redo log 持久化之后,意味着事务是可提交

悲观锁和乐观锁

参考链接:https://cloud.tencent.com/developer/article/1450773

悲观锁

image-20210912202607757

正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。

悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机 制,也无法保证外部系统不会修改数据)。

在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。

每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁表锁等,读锁写锁等,都是在做操作之前先上锁。

乐观锁

image-20210912202630691

相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。

何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。

此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据

每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

执行事务顺序

  1. 关闭自动提交:SET autocommit = 0
  2. 开启一个事务:START TRANSACTION
    • 提交(执行成功):COMMIT ,获得新的数据库
    • 回滚(执行失败):ROLLBACK,返回原先数据库
  3. 开启自动提交:SET autocommit = 1
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
-- MySQL是默认开启事务自动提交的
SET autocommit = 0; -- 关闭
SET autocommit = 1; -- 开启(默认)

-- 手动处理事务
SET autocommit = 0; -- 关闭自动提交

-- 事务开启
START TRANSACTION; -- 标记一个事务的开始,从这个之后的sql都在同一个事务内

INSERT xx;
INSERT xx;

-- 提交:持久化(成功)
COMMIT;

-- 回滚:回到原来的样子(失败)
ROLLBACK;

-- 事务结束
SET autocommit = 1; -- 开启自动提交

SAVEPOINT 保存点名称a; -- 设置一个事务的保存点
ROLLBACK TO 保存点名称a; -- 回滚到保存点
RELEASE SAVEPOINT 保存点名称; -- 撤销保存点

测试案例:

1
2
3
4
5
6
7
8
9
10
11
12
-- 模拟转账
SET autocommit = 0; -- 关闭自动提交

START TRANSACTION; -- 开启一个事务

UPDATE `account` SET `money`=`money` - 500 WHERE `name` = 'zhangsan';
UPDATE `account` SET `money`=`money` + 500 WHERE `name` = 'lisi';

COMMIT; -- 提交事务,执行后数据库内容才会修改
ROLLBACK; -- 回滚,数据库内容不会修改

SET autocommit = 1; -- 恢复默认值

DELETE 和 TRUNCATE 在事务中的区别

  • DELETE在事务提交前使用,若回滚,则数据会恢复
  • TRUNCATE事务提交前使用,若回滚,则数据依旧不会恢复

【MySQL】JDBC

JDBC简介

JDBC概念:https://www.jianshu.com/p/e71990336319

参考视频:https://www.bilibili.com/video/BV1eJ411c7rf?p=1

JDBC(Java Data Base Connectivity,java数据库连接)是一个独立于特定数据库管理系统、通用的SQL数据库存取和操作的公共接口(一组API)。,定义了用来访问数据库的标准Java类库,(java.sql,javax.sql)使用这些类库可以以一种标准的方法、方便地访问数据库资源。

普通应用程序无法直接和数据库进行通讯连接,需要借助数据库驱动和数据库连接。SUN公司为了简化开发人员对数据库的统一操作,提供了一个Java操作数据库的规范,俗称JDBC。这些规范由具体的厂商去做,开发人员只需要调用接口即可连接数据库进行开发。

JDBC为访问不同的数据库提供了一种统一的途径,为开发者屏蔽了一些细节问题。JDBC的目标是使Java程序员使用JDBC可以连接任何提供了JDBC驱动程序的数据库系统,这样就使得程序员无需对特定的数据库系统的特点有过多的了解,从而大大简化和加快了开发过程。

应用程序 ——> JDBC ——> MySQL驱动 /Oracle驱动 ——> MySQL数据库/Oracle数据库

image-20210517213554324

数据库应用厂商实现JDBC的接口。开发人员不需要关注每种数据库的具体实现。

JDBC是sun公司提供一套用于数据库操作的接口,java程序员只需要面向这套接口编程即可。

不同的数据库厂商,需要针对这套接口,提供不同实现。不同的实现的集合,即为不同数据库的驱动。 ————面向接口编程

1、JDBC使用概览

JDBC使用步骤:

  • 加载驱动:Class.forName("com.mysql.jdbc.Driver");
  • 连接数据库:DriverManager.getConnection(url, username, password);
  • 创建执行sql语句的对象:Statement statement = connection.createStatement();
  • 获得返回的结果集:ResultSet resultSet = statement.executeQuery(sql);
  • 释放连接:resultSet.close();

image-20210709164526603

阅读全文

【MySQL】MySQL 基础

image-20210913132511709

1. 初识 MySQL

  • DB:DataBase,数据库,实际上在硬盘上以文件的形式存在
  • SQL:Structured Query Language,结构化查询语言,是一门标准通用的语言,标准sql适用于所有的数据库产品,SQL语句在执行时,内部会先进行编译,然后再执行。(SQL语句的编译由DBMS完成)
  • DBMS: DataBase Management System,数据库管理系统,如MySQL,Oracle,DB2等,用来管理DB(用来管理数据库中的数据)

DBMS负责执行sql语句,其通过执行sql语句来操作DB中的数据。

1.1 启动 MySQL 服务

命令行启动MySQL服务(管理员模式)

1
net start mysql

1.2 连接数据库

命令行连接数据库(管理员模式)

1
mysql -u root -p xxxxx  -- 连接数据库

1.3 SQL 命令

更改用户密码并刷新权限(sql语句需要以;结尾)

1
2
3
4
UPDATE mysql.user SET authentication_string=password('123456')
WHERE user='root' AND Host='localhost'; -- 更改用户密码

FLUSH PRIVILEGES; -- 刷新权限
阅读全文

【Java】稀疏矩阵

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 SparesArray {
public static void main(String[] args) {
// 创建一个二维数组 11*11 0:没有旗子, 1:黑棋, 2:白棋
int[][] array1 = new int[11][11];
array1[1][2] = 1;
array1[2][3] = 2;

System.out.println("输出原始的矩阵");

for (int[] ints : array1){
for (int anInt : ints){
System.out.print(anInt + "\t");
}
System.out.println();
}

// 转换为稀疏数组
// 1. 获取有效值的个数
int sum = 0;
for (int i = 0; i < 11; i++){
for (int j = 0; j < 11; j++) {
if (array1[i][j] != 0) {
sum++;
}
}
}
System.out.println("有效值个数:" + sum);

// 2. 创建一个稀疏数组的数组
int[][] array2 = new int[sum+1][3];
array2[0][0] = 11;
array2[0][1] = 11;
array2[0][2] = sum;

// 遍历二维数组,将非零值放入稀疏数组
int count = 0;
for (int i = 0; i < array1.length; i++){
for (int j = 0; j < array1[i].length; j++) {
if (array1[i][j] != 0){
count++;
array2[count][0] = i;
array2[count][1] = j;
array2[count][2] = array1[i][j];
}
}
}

System.out.println("稀疏数组");
for (int i = 0; i < array2.length; i++){
System.out.println(array2[i][0] + "\t" + array2[i][1] + "\t" + array2[i][2]);
}

// ===============================================================
// 稀疏数组还原
System.out.println("还原");

// 1. 读取稀疏数组
int[][] array3 = new int[array2[0][0]][array2[0][1]];

// 2. 给其中元素还原它的值
for (int i = 1; i < array2.length; i++){
array3[array2[i][0]][array2[i][1]] = array2[i][2];
}

// 3. 打印
for (int[] ints : array3){
for (int anInt : ints){
System.out.print(anInt + "\t");
}
System.out.println();
}

}
}

【Java】Stream API

Stream API

Java8中有两大最为重要的改变。第一个是 Lambda 表达式;另外一个则是 Stream API。 Stream API ( java.util.stream) 把真正的函数式编程风格引入到Java中。这是目前为止对Java类库最好的补充,因为Stream API可以极大提供Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。

Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找过滤映射数据等操作。 使用 Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。 也可以使用 Stream API 来并行执行操作。简言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

Stream 是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。 “集合讲的是数据,Stream讲的是计算!” 注意:

  • Stream 自己不会存储元素
  • Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
  • Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。

为什么要是用 Stream API

实际开发中,项目中多数数据源都来自于Mysql,Oracle等。但现在数据源可以更多了,有MongDB,Radis 等,而这些 NoSQL 的数据就需要 Java 层面去处理。

Stream 和 Collection 集合的区别:Collection 是一种静态的内存数据结构,而 Stream 是有关计算的。前者是主要面向内存,存储在内存中, 后者主要是面向 CPU,通过 CPU 实现计算。

Stream 的使用流程

image-20210626155810489

使用流程的注意点:

  • 一个中间操作链,对数据源的数据进行处理
  • 一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用

创建 Stream 的方式

方式一:通过集合

image-20210626160117139

1
2
3
4
5
6
7
8
9
10
11
//创建Stream方式一:通过集合
@Test
public void test1(){
List<Employee> employees = EmployeeData.getEmployees();

//default Stream<E> stream() : 返回一个顺序流
Stream<Employee> stream = employees.stream();

//default Stream<E> parallelStream() : 返回一个并行流
Stream<Employee> parallelStream = employees.parallelStream();
}

方式二:通过数组

image-20210626160210175

1
2
3
4
5
6
7
8
9
10
11
12
//创建Stream方式二:通过数组
@Test
public void test2(){
int[] arr = new int[]{1,2,3,4,5,6};
//调用Arrays类的static <T> Stream<T> stream(T[] array): 返回一个流
IntStream stream = Arrays.stream(arr);

Employee e1 = new Employee(1001,"Tom");
Employee e2 = new Employee(1002,"Jerry");
Employee[] arr1 = new Employee[]{e1,e2};
Stream<Employee> stream1 = Arrays.stream(arr1);
}

方式三:通过 Stream 的 of()

image-20210626160255007

1
2
3
4
5
//创建Stream方式三:通过Stream的of()
@Test
public void test3(){
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
}

方式四:创建无限流

image-20210626160338594

1
2
3
4
5
6
7
8
9
10
11
12
//创建Stream方式四:创建无限流
@Test
public void test4(){
//迭代
//public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
//遍历前10个偶数
Stream.iterate(0, t -> t + 2).limit(10).forEach(System.out::println);

//生成
//public static<T> Stream<T> generate(Supplier<T> s)
Stream.generate(Math::random).limit(10).forEach(System.out::println);
}

总结四种方式:

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
//创建Stream方式一:通过集合
@Test
public void test1(){
List<Employee> employees = EmployeeData.getEmployees();

//default Stream<E> stream() : 返回一个顺序流
Stream<Employee> stream = employees.stream();

//default Stream<E> parallelStream() : 返回一个并行流
Stream<Employee> parallelStream = employees.parallelStream();
}

//创建Stream方式二:通过数组
@Test
public void test2(){
int[] arr = new int[]{1,2,3,4,5,6};
//调用Arrays类的static <T> Stream<T> stream(T[] array): 返回一个流
IntStream stream = Arrays.stream(arr);

Employee e1 = new Employee(1001,"Tom");
Employee e2 = new Employee(1002,"Jerry");
Employee[] arr1 = new Employee[]{e1,e2};
Stream<Employee> stream1 = Arrays.stream(arr1);
}

//创建Stream方式三:通过Stream的of()
@Test
public void test3(){
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
}

//创建Stream方式四:创建无限流
@Test
public void test4(){
//迭代
//public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
//遍历前10个偶数
Stream.iterate(0, t -> t + 2).limit(10).forEach(System.out::println);

//生成
//public static<T> Stream<T> generate(Supplier<T> s)
Stream.generate(Math::random).limit(10).forEach(System.out::println);
}

Stream 中间操作

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”。

image-20210626160546946

image-20210626160550965

image-20210626160554806

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
public class StreamAPITest1 {

//1-筛选与切片
@Test
public void test1(){
List<Employee> list = EmployeeData.getEmployees();
// filter(Predicate p)——接收 Lambda , 从流中排除某些元素。
Stream<Employee> stream = list.stream();
//练习:查询员工表中薪资大于7000的员工信息
stream.filter(e -> e.getSalary() > 7000).forEach(System.out::println);

System.out.println();
//limit(n)——截断流,使其元素不超过给定数量。
list.stream().limit(3).forEach(System.out::println);
System.out.println();

//skip(n) —— 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补
list.stream().skip(3).forEach(System.out::println);

System.out.println();
//distinct()——筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素

list.add(new Employee(1010,"刘强东",40,8000));
list.add(new Employee(1010,"刘强东",41,8000));
list.add(new Employee(1010,"刘强东",40,8000));
list.add(new Employee(1010,"刘强东",40,8000));
list.add(new Employee(1010,"刘强东",40,8000));

//System.out.println(list);

list.stream().distinct().forEach(System.out::println);
}

//映射
@Test
public void test2(){
//map(Function f)——接收一个函数作为参数,将元素转换成其他形式或提取信息,该函数会被应用到每个元素上,并将其映射成一个新的元素。
List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
list.stream().map(str -> str.toUpperCase()).forEach(System.out::println);

//练习1:获取员工姓名长度大于3的员工的姓名。
List<Employee> employees = EmployeeData.getEmployees();
Stream<String> namesStream = employees.stream().map(Employee::getName);
namesStream.filter(name -> name.length() > 3).forEach(System.out::println);
System.out.println();
//练习2:
Stream<Stream<Character>> streamStream = list.stream().map(StreamAPITest1::fromStringToStream);
streamStream.forEach(s ->{
s.forEach(System.out::println);
});
System.out.println();
//flatMap(Function f)——接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
Stream<Character> characterStream = list.stream().flatMap(StreamAPITest1::fromStringToStream);
characterStream.forEach(System.out::println);

}

//将字符串中的多个字符构成的集合转换为对应的Stream的实例
public static Stream<Character> fromStringToStream(String str){//aa
ArrayList<Character> list = new ArrayList<>();
for(Character c : str.toCharArray()){
list.add(c);
}
return list.stream();
}

@Test
public void test3(){
ArrayList list1 = new ArrayList();
list1.add(1);
list1.add(2);
list1.add(3);

ArrayList list2 = new ArrayList();
list2.add(4);
list2.add(5);
list2.add(6);

//list1.add(list2);
list1.addAll(list2);
System.out.println(list1);
}

//3-排序
@Test
public void test4(){
//sorted()——自然排序
List<Integer> list = Arrays.asList(12, 43, 65, 34, 87, 0, -98, 7);
list.stream().sorted().forEach(System.out::println);
//抛异常,原因:Employee没有实现Comparable接口
//List<Employee> employees = EmployeeData.getEmployees();
//employees.stream().sorted().forEach(System.out::println);

//sorted(Comparator com)——定制排序

List<Employee> employees = EmployeeData.getEmployees();
employees.stream().sorted( (e1,e2) -> {

int ageValue = Integer.compare(e1.getAge(),e2.getAge());
if(ageValue != 0){
return ageValue;
}else{
return -Double.compare(e1.getSalary(),e2.getSalary());
}

}).forEach(System.out::println);
}
}

Stream 终止操作

终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:ListInteger,甚至是 void。流进行了终止操作后,不能再次使用。

image-20210626160640867

image-20210626160645113

image-20210626160649435

image-20210626160653025

Collector需要使用Collectors提供实例。

image-20210626160700820

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 StreamAPITest2 {

//1-匹配与查找
@Test
public void test1(){
List<Employee> employees = EmployeeData.getEmployees();

//allMatch(Predicate p)——检查是否匹配所有元素。
//练习:是否所有的员工的年龄都大于18
boolean allMatch = employees.stream().allMatch(e -> e.getAge() > 18);
System.out.println(allMatch);

//anyMatch(Predicate p)——检查是否至少匹配一个元素。
//练习:是否存在员工的工资大于 10000
boolean anyMatch = employees.stream().anyMatch(e -> e.getSalary() > 10000);
System.out.println(anyMatch);

//noneMatch(Predicate p)——检查是否没有匹配的元素。
//练习:是否存在员工姓“雷”
boolean noneMatch = employees.stream().noneMatch(e -> e.getName().startsWith("雷"));
System.out.println(noneMatch);
//findFirst——返回第一个元素
Optional<Employee> employee = employees.stream().findFirst();
System.out.println(employee);
//findAny——返回当前流中的任意元素
Optional<Employee> employee1 = employees.parallelStream().findAny();
System.out.println(employee1);
}

@Test
public void test2(){
List<Employee> employees = EmployeeData.getEmployees();
// count——返回流中元素的总个数
long count = employees.stream().filter(e -> e.getSalary() > 5000).count();
System.out.println(count);
//max(Comparator c)——返回流中最大值
//练习:返回最高的工资:
Stream<Double> salaryStream = employees.stream().map(e -> e.getSalary());
Optional<Double> maxSalary = salaryStream.max(Double::compare);
System.out.println(maxSalary);
//min(Comparator c)——返回流中最小值
//练习:返回最低工资的员工
Optional<Employee> employee = employees.stream().min((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));
System.out.println(employee);
System.out.println();
//forEach(Consumer c)——内部迭代
employees.stream().forEach(System.out::println);

//使用集合的遍历操作
employees.forEach(System.out::println);
}

//2-归约
@Test
public void test3(){
//reduce(T identity, BinaryOperator)——可以将流中元素反复结合起来,得到一个值。返回 T
//练习1:计算1-10的自然数的和
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
Integer sum = list.stream().reduce(0, Integer::sum);
System.out.println(sum);

//reduce(BinaryOperator) ——可以将流中元素反复结合起来,得到一个值。返回 Optional<T>
//练习2:计算公司所有员工工资的总和
List<Employee> employees = EmployeeData.getEmployees();
Stream<Double> salaryStream = employees.stream().map(Employee::getSalary);
//Optional<Double> sumMoney = salaryStream.reduce(Double::sum);
Optional<Double> sumMoney = salaryStream.reduce((d1,d2) -> d1 + d2);
System.out.println(sumMoney.get());
}

//3-收集
@Test
public void test4(){
//collect(Collector c)——将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法
//练习1:查找工资大于6000的员工,结果返回为一个List或Set
List<Employee> employees = EmployeeData.getEmployees();
List<Employee> employeeList = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toList());

employeeList.forEach(System.out::println);
System.out.println();
Set<Employee> employeeSet = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toSet());

employeeSet.forEach(System.out::println);
}
}

Optional 类

Optional 类:为了解决java中的空指针问题而生。

到目前为止,臭名昭著的空指针异常是导致Java应用程序失败的最常见原因。 以前,为了解决空指针异常,Google公司著名的Guava项目引入了Optional类,Guava通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代 码。受到Google Guava的启发,Optional类已经成为Java 8类库的一部分。

Optional 类(java.util.Optional) 是一个容器类,它可以保存类型T的值,代表这个值存在。或者仅仅保存 null,表示这个值不存在。原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且可以避免空指针异常。

Optional类的 Javadoc 描述如下:这是一个可以为 null 的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。

image-20210626162456425

使用举例:

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
@Test
public void test1(){
//empty():创建的Optional对象内部的value = null
Optional<Object> op1 = Optional.empty();
if(!op1.isPresent()){//Optional封装的数据是否包含数据
System.out.println("数据为空");

}
System.out.println(op1);
System.out.println(op1.isPresent());
//如果Optional封装的数据value为空,则get()报错。否则,value不为空时,返回value.
//System.out.println(op1.get());

}

@Test
public void test2(){
String str = "hello";
//str = null;
//of(T t):封装数据t生成Optional对象。要求t非空,否则报错。
Optional<String> op1 = Optional.of(str);
//get()通常与of()方法搭配使用。用于获取内部的封装的数据value
String str1 = op1.get();
System.out.println(str1);

}

@Test
public void test3(){
String str = "beijing";
str = null;
//ofNullable(T t) :封装数据t赋给Optional内部的value。不要求t非空
Optional<String> op1 = Optional.ofNullable(str);
//orElse(T t1):如果Optional内部的value非空,则返回此value值。如果
//value为空,则返回t1.
String str2 = op1.orElse("shanghai");

System.out.println(str2);//
}

【Java】Lambda 表达式

Lambda 表达式

Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。使用它可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升、

从匿名内部类到 Lambda 的转换举例1:

匿名内部类形式:

1
2
3
4
5
6
7
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("我爱北京天安门");
}
};
r1.run();

Lambda 表达式形式:

1
2
Runnable r2 = () -> System.out.println("我爱北京故宫");
r2.run();

从匿名内部类到Lambda 的转换举例2:

匿名内部类形式:

1
2
3
4
5
6
7
Comparator<Integer> com1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1,o2);
}
};
int compare1 = com1.compare(12,21);

Lambda 表达式形式:

1
2
Comparator<Integer> com2 = (o1,o2) -> Integer.compare(o1,o2);
int compare2 = com2.compare(32,21);

方法引用形式:

1
2
Comparator<Integer> com3 = Integer :: compare;
int compare3 = com3.compare(32,21);

语法

Lambda 表达式:在Java 8语言中引入的一种新的语法元素和操作符。这个操作符为 “->” , 该操作符被称为 Lambda 操作符或箭头操作符。它将 Lambda 分为两个部分:

  • 左侧:指定了 Lambda 表达式需要的参数列表
  • 右侧:指定了 Lambda体 ,是抽象方法的实现逻辑,也即 Lambda 表达式要执行的功能。

六种使用方式

image-20210626150542844

image-20210626150547722

总结六种情况:

  • 左边:lambda 形参列表的参数类型可以省略(类型推断);如果 lambda 形参列表只一个参数,其一对()也可以省略
  • 右边:lambda 体应该使用一对{}包裹;如果 lambda 体只一条执行语句(可能是return语句,省略这一对{}和return关键字)

类型推断

上述 Lambda 表达式中的参数类型都是由编译器推断得出的。Lambda 表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,在后台推断出了参数的类型。Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的“类型推断”。

image-20210626150920181

函数式接口

定义

只包含一个抽象方法的接口,称为函数式接口

  • 可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda 表达式抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽象方法上进行声明)。
  • 可以在一个接口上使用@FunctionalInterface 注解,这样可以检查它是否是一个函数式接口。
  • 同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。
  • java.util.function包下定义了Java 8 的丰富的函数式接口

如何理解函数式接口

Java从诞生日起就是一直倡导“一切皆对象”,在Java里面面向对象(OOP) 编程是一切。但是随着python、scala等语言的兴起和新技术的挑战,Java不得不做出调整以便支持更加广泛的技术要求,也即java不但可以支持OOP还 可以支持OOF(面向函数编程)

在函数式编程语言当中,函数被当做一等公民对待。在将函数作为一等公民的编程语言中,Lambda表达式的类型是函数。但是在Java8中,有所不同。在 Java8中,Lambda表达式是对象,而不是函数,它们必须依附于一类特别的 对象类型——函数式接口。

简单的说,在Java8中,Lambda表达式就是一个函数式接口的实例。这就是 Lambda表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口的实例,那么该对象就可以用 Lambda 表达式来表示。

所以以前用匿名实现类表示的现在都可以用 Lambda 表达式来写。

Lambda表达式的本质:作为函数式接口的实例

函数式接口举例:

image-20210626151346747

image-20210626151503858

Java 内置四大核心函数式接口

image-20210626153213826

其他函数式接口

image-20210626153330040

方法引用

方法引用可以看做是 Lambda 表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法。

  • 当要传递给 Lambda 体的操作,已经有实现的方法了,可以使用方法引用。
  • 要求:实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致。
  • 格式:使用操作符 “::” 将类(或对象) 与方法名分隔开来。
  • 如下三种主要使用情况:
    • 情况一:对象::实例方法名
    • 情况二:类::静态方法名
    • 情况三:类::实例方法名

要求接口中的抽象方法的形参列表和返回值类型与方法引用的方法的形参列表和返回值类型相同!(针对于情况1和情况2)当函数式接口方法的第一个参数是需要引用方法的调用者,并且第二个参数是需要引用方法的参数(或无参数)时:ClassName::methodName(针对于情况3)

image-20210626153934414

image-20210626154023663

使用举例:

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
// 情况一:对象 :: 实例方法
//Consumer中的void accept(T t)
//PrintStream中的void println(T t)
@Test
public void test1() {
Consumer<String> con1 = str -> System.out.println(str);
con1.accept("北京");

System.out.println("*******************");
PrintStream ps = System.out;
Consumer<String> con2 = ps::println;
con2.accept("beijing");
}

//Supplier中的T get()
//Employee中的String getName()
@Test
public void test2() {
Employee emp = new Employee(1001,"Tom",23,5600);

Supplier<String> sup1 = () -> emp.getName();
System.out.println(sup1.get());

System.out.println("*******************");
Supplier<String> sup2 = emp::getName;
System.out.println(sup2.get());

}

// 情况二:类 :: 静态方法
//Comparator中的int compare(T t1,T t2)
//Integer中的int compare(T t1,T t2)
@Test
public void test3() {
Comparator<Integer> com1 = (t1,t2) -> Integer.compare(t1,t2);
System.out.println(com1.compare(12,21));

System.out.println("*******************");

Comparator<Integer> com2 = Integer::compare;
System.out.println(com2.compare(12,3));

}

//Function中的R apply(T t)
//Math中的Long round(Double d)
@Test
public void test4() {
Function<Double,Long> func = new Function<Double, Long>() {
@Override
public Long apply(Double d) {
return Math.round(d);
}
};

System.out.println("*******************");

Function<Double,Long> func1 = d -> Math.round(d);
System.out.println(func1.apply(12.3));

System.out.println("*******************");

Function<Double,Long> func2 = Math::round;
System.out.println(func2.apply(12.6));
}

// 情况:类 :: 实例方法 (难度)
// Comparator中的int comapre(T t1,T t2)
// String中的int t1.compareTo(t2)
@Test
public void test5() {
Comparator<String> com1 = (s1,s2) -> s1.compareTo(s2);
System.out.println(com1.compare("abc","abd"));

System.out.println("*******************");

Comparator<String> com2 = String :: compareTo;
System.out.println(com2.compare("abd","abm"));
}

//BiPredicate中的boolean test(T t1, T t2);
//String中的boolean t1.equals(t2)
@Test
public void test6() {
BiPredicate<String,String> pre1 = (s1,s2) -> s1.equals(s2);
System.out.println(pre1.test("abc","abc"));

System.out.println("*******************");
BiPredicate<String,String> pre2 = String :: equals;
System.out.println(pre2.test("abc","abd"));
}

// Function中的R apply(T t)
// Employee中的String getName();
@Test
public void test7() {
Employee employee = new Employee(1001, "Jerry", 23, 6000);

Function<Employee,String> func1 = e -> e.getName();
System.out.println(func1.apply(employee));

System.out.println("*******************");

Function<Employee,String> func2 = Employee::getName;
System.out.println(func2.apply(employee));

}

构造器引用

image-20210626154608885

使用举例:

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
//Supplier中的T get()
//Employee的空参构造器:Employee()
@Test
public void test1(){

Supplier<Employee> sup = new Supplier<Employee>() {
@Override
public Employee get() {
return new Employee();
}
};
System.out.println("*******************");

Supplier<Employee> sup1 = () -> new Employee();
System.out.println(sup1.get());

System.out.println("*******************");

Supplier<Employee> sup2 = Employee :: new;
System.out.println(sup2.get());
}

//Function中的R apply(T t)
@Test
public void test2(){
Function<Integer,Employee> func1 = id -> new Employee(id);
Employee employee = func1.apply(1001);
System.out.println(employee);

System.out.println("*******************");

Function<Integer,Employee> func2 = Employee :: new;
Employee employee1 = func2.apply(1002);
System.out.println(employee1);

}

//BiFunction中的R apply(T t,U u)
@Test
public void test3(){
BiFunction<Integer,String,Employee> func1 = (id,name) -> new Employee(id,name);
System.out.println(func1.apply(1001,"Tom"));

System.out.println("*******************");

BiFunction<Integer,String,Employee> func2 = Employee :: new;
System.out.println(func2.apply(1002,"Tom"));

}

数组引用

image-20210626154622412

使用举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
//Function中的R apply(T t)
@Test
public void test4(){
Function<Integer,String[]> func1 = length -> new String[length];
String[] arr1 = func1.apply(5);
System.out.println(Arrays.toString(arr1));

System.out.println("*******************");

Function<Integer,String[]> func2 = String[] :: new;
String[] arr2 = func2.apply(10);
System.out.println(Arrays.toString(arr2));
}

【Java】网络编程

InetAddress

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
public static void main(String[] args) {

try {
//File file = new File("hello.txt");
InetAddress inet1 = InetAddress.getByName("192.168.10.14");

System.out.println(inet1);

InetAddress inet2 = InetAddress.getByName("www.baidu.com");
System.out.println(inet2);

InetAddress inet3 = InetAddress.getByName("127.0.0.1");
System.out.println(inet3);

//获取本地ip
InetAddress inet4 = InetAddress.getLocalHost();
System.out.println(inet4);

//getHostName()
System.out.println(inet2.getHostName());
//getHostAddress()
System.out.println(inet2.getHostAddress());

} catch (UnknownHostException e) {
e.printStackTrace();
}
}

TCP Socket

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
public class TCPTest1 {

//客户端
@Test
public void client() {
Socket socket = null;
OutputStream os = null;
try {
//1.创建Socket对象,指明服务器端的ip和端口号
InetAddress inet = InetAddress.getByName("192.168.14.100");
socket = new Socket(inet,8899);
//2.获取一个输出流,用于输出数据
os = socket.getOutputStream();
//3.写出数据的操作
os.write("你好,我是客户端mm".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.资源的关闭
if(os != null){
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}

}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}

}
}



}
//服务端
@Test
public void server() {

ServerSocket ss = null;
Socket socket = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
try {
//1.创建服务器端的ServerSocket,指明自己的端口号
ss = new ServerSocket(8899);
//2.调用accept()表示接收来自于客户端的socket
socket = ss.accept();
//3.获取输入流
is = socket.getInputStream();

//不建议这样写,可能会有乱码
// byte[] buffer = new byte[1024];
// int len;
// while((len = is.read(buffer)) != -1){
// String str = new String(buffer,0,len);
// System.out.print(str);
// }
//4.读取输入流中的数据
baos = new ByteArrayOutputStream();
byte[] buffer = new byte[5];
int len;
while((len = is.read(buffer)) != -1){
baos.write(buffer,0,len);
}

System.out.println(baos.toString());

System.out.println("收到了来自于:" + socket.getInetAddress().getHostAddress() + "的数据");

} catch (IOException e) {
e.printStackTrace();
} finally {
if(baos != null){
//5.关闭资源
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(is != null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(ss != null){
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

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
public class TCPTest2 {

/*
这里涉及到的异常,应该使用try-catch-finally处理
*/
@Test
public void client() throws IOException {
//1.
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),9090);
//2.
OutputStream os = socket.getOutputStream();
//3.
FileInputStream fis = new FileInputStream(new File("beauty.jpg"));
//4.
byte[] buffer = new byte[1024];
int len;
while((len = fis.read(buffer)) != -1){
os.write(buffer,0,len);
}
//关闭数据的输出
socket.shutdownOutput();

//5.接收来自于服务器端的数据,并显示到控制台上
InputStream is = socket.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] bufferr = new byte[20];
int len1;
while((len1 = is.read(buffer)) != -1){
baos.write(buffer,0,len1);
}

System.out.println(baos.toString());

//6.
fis.close();
os.close();
socket.close();
baos.close();
}

/*
这里涉及到的异常,应该使用try-catch-finally处理
*/
@Test
public void server() throws IOException {
//1.
ServerSocket ss = new ServerSocket(9090);
//2.
Socket socket = ss.accept();
//3.
InputStream is = socket.getInputStream();
//4.
FileOutputStream fos = new FileOutputStream(new File("beauty.jpg"));
//5.
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer)) != -1){
fos.write(buffer,0,len);
}

System.out.println("图片传输完成");

//6.服务器端给予客户端反馈
OutputStream os = socket.getOutputStream();
os.write("你好!".getBytes());

//7.
fos.close();
is.close();
socket.close();
ss.close();
os.close();

}
}

UDP DatagramSocket

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
public class UDPTest {

//发送端
@Test
public void sender() throws IOException {

DatagramSocket socket = new DatagramSocket();

String str = "我是UDP方式发送的导弹";
byte[] data = str.getBytes();
InetAddress inet = InetAddress.getLocalHost();
DatagramPacket packet = new DatagramPacket(data,0,data.length,inet,9090);

socket.send(packet);

socket.close();

}
//接收端
@Test
public void receiver() throws IOException {

DatagramSocket socket = new DatagramSocket(9090);

byte[] buffer = new byte[100];
DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length);

socket.receive(packet);

System.out.println(new String(packet.getData(),0,packet.getLength()));

socket.close();
}
}

URL

URL:统一资源定位符,对应着互联网的某一资源地址。

格式:http://localhost:8080/examples/beauty.jpg?username=Tom

协议 主机名 端口号 资源地址 参数列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class URLTest {

public static void main(String[] args) {
try {
URL url = new URL("http://localhost:8080/examples/beauty.jpg?username=Tom");
// public String getProtocol( ) 获取该URL的协议名
System.out.println(url.getProtocol());
// public String getHost( ) 获取该URL的主机名
System.out.println(url.getHost());
// public String getPort( ) 获取该URL的端口号
System.out.println(url.getPort());
// public String getPath( ) 获取该URL的文件路径
System.out.println(url.getPath());
// public String getFile( ) 获取该URL的文件名
System.out.println(url.getFile());
// public String getQuery( ) 获取该URL的查询名
System.out.println(url.getQuery());
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}