开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Java 16 新功能介绍

发表于 2021-11-01

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

点赞再看,动力无限。Hello world : ) 微信搜「 程序猿阿朗 」。

本文 Github.com/niumoo/Java… 和 程序猿阿朗博客 已经收录,有很多知识点和系列文章。

Java 16 在 2021 年 3 月 16 日正式发布,不是长久支持版本,这次更新没有带来很多语法上的改动,但是也带来了不少新的实用功能。

OpenJDK Java 16 下载:jdk.java.net/archive/

OpenJDK Java 16 文档:openjdk.java.net/projects/jd…

此文章属于 Java 新特性教程 系列,会介绍 Java 每个版本的新功能,可以点击浏览。

  1. JEP 347: 启用 C++ 14 语言特性

这项更新和 Java 开发者关系不太密切,JEP 347 允许 在 JDK 的 C++ 源码中使用 C++ 14 的语言特性,并且给出了哪些特性可以在 HotSpot 代码中使用的具体说明。

扩展阅读:启用 C++ 14 语言特性

  1. JEP 357:从 Mercurial 迁移到 Git

在此之前,OpenJDK 源代码是使用版本管理工具 Mercurial 进行管理的,你也可以在 hg.openjdk.java.net/ 查看 OpenJDK 的源代码历史版本。

但是现在迁移到了 GIt ,主要原因如下:

  1. Mercurial 生成的版本控制元数据过大。
  2. Mercurial 相关的开发工具比较少,而 Git 几乎在所有的主流 IDE 中已经无缝集成。
  3. Mercurial 相关的服务比较少,无论是自建托管,还是服务托管。

为了优雅的迁移到 Git,OpenJDK 做了如下操作。

  1. 将所有的单存储库 OpenJDK 项目从 Mercurial 迁移到 Git。
  2. 保留所有的版本控制历史,也包括 Tag。
  3. 根据 Git 的最佳实践重新格式化提交的消息。
  4. 创建了一个工具用来在 Mercurial 和 Git 哈希之间进行转换。

扩展阅读: 从 Mercurial 迁移到 Git

  1. JEP 369:迁移到 GitHub

和 JEP 357 从 Mercurial 迁移到 Git 的改变一致,在把版本管理迁移到 Git 之后,选择了在 GitHub 上托管 OpenJDK 社区的 Git 仓库。不过只对 JDK 11 以及更高版本 JDK 进行了迁移。

  1. JEP 376:ZGC 并发线程堆栈处理

这次改动让 ZGC 线程堆栈处理从**安全点(Safepoints)**移动到并发阶段。

如果你忘记了什么是 Safepoints,可以复习一下。

我们都知道,在之前,需要 GC 的时候,为了进行垃圾回收,需要所有的线程都暂停下来,这个暂停的时间我们成为 Stop The World。

而为了实现 STW 这个操作, JVM 需要为每个线程选择一个点停止运行,这个点就叫做安全点(Safepoints)。

扩展阅读:JEP 376:ZGC 并发线程堆栈处理

  1. JEP 380:Unix 域套接字通道

添加 UnixDomainSocketAddress.java 类用于支持 Unix 域套接字通道。

添加 Unix-domain socket 到 SocketChannel 和 ServerSocketChannel API 中。

添加枚举信息 java.net.StandardProtocolFamily.UNIX。

  1. JEP 386:移植 Alpine Linux

Apine Linux 是一个独立的、非商业的 Linux 发行版,它十分的小,一个容器需要不超过 8MB 的空间,最小安装到磁盘只需要大约 130MB 存储空间,并且十分的简单,同时兼顾了安全性。

此提案将 JDK 移植到了 Apline Linux,由于 Apline Linux 是基于 musl lib 的轻量级 Linux 发行版,因此其他 x64 和 AArch64 架构上使用 musl lib 的 Linux 发行版也适用。

扩展阅读:JEP 386: Alpine Linux Port

  1. JEP 387:更好的 Metaspace

自从引入了 Metaspace 以来,根据反馈,Metaspace 经常占用过多的堆外内存,从而导致内存浪费,现在可以更及时地将未使用的 HotSpot class-metaspace 内存返还给操作系统,从而减少 Metaspace 的占用空间,并优化了 Metaspace 代码以降低后续的维护成本。

  1. JEP 388:移植 Windows/AArch64

将 JDK 移植到 Windows/AArch64 架构上,Windows/AArch64 已经是终端用户市场的热门需求。

  1. JEP 389:外部连接器 API(孵化)

这项提案让 Java 代码可以调用由其他语言(比如 C ,C++)编写的编译后的机器代码,替换了之前的 JNI 形式。

不过这还是一个孵化中的功能,运行时需要添加 --add-modules jdk.incubator.foreign 参数来编译和运行 Java 代码。

下面是一个调用 C 语言函数方法,然后输出运行结果的例子。

  1. 编写一个 C 函数打印一个 “hello www.wdbyte.com”。
1
2
3
4
5
c复制代码#include <stdio.h>

void printHello(){
printf("hello www.wdbyte.com\n");
}
  1. 将上面的代码编译,然后输出到共享库 hello.so
1
2
3
4
5
6
7
shell复制代码$ gcc -c -fPIC hello.c
$ gcc -shared -o hello.so hello.o
$ ll
total 128
-rw-r--r-- 1 darcy staff 76B 10 28 19:46 hello.c
-rw-r--r-- 1 darcy staff 776B 10 28 19:46 hello.o
-rwxr-xr-x 1 darcy staff 48K 10 28 19:47 hello.so
  1. 编写一个 Java 代码,调用 hello.so 的 printHello 方法。
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
java复制代码import jdk.incubator.foreign.CLinker;
import jdk.incubator.foreign.FunctionDescriptor;
import jdk.incubator.foreign.LibraryLookup;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.nio.file.Path;
import java.util.Optional;

public class JEP389 {

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

Path path = Path.of("/Users/darcy/git/java-core/java-16/src/com/wdbyte/hello.so");

LibraryLookup libraryLookup = LibraryLookup.ofPath(path);

Optional<LibraryLookup.Symbol> optionalSymbol = libraryLookup.lookup("printHello");
if (optionalSymbol.isPresent()) {

LibraryLookup.Symbol symbol = optionalSymbol.get();

FunctionDescriptor functionDescriptor = FunctionDescriptor.ofVoid();

MethodType methodType = MethodType.methodType(Void.TYPE);

MethodHandle methodHandle = CLinker.getInstance().downcallHandle(
symbol.address(),
methodType,
functionDescriptor);
methodHandle.invokeExact();

}

}
}
  1. Java 代码编译。
1
2
3
shell复制代码$ javac --add-modules jdk.incubator.foreign JEP389.java
警告: 使用 incubating 模块: jdk.incubator.foreign
1 个警告
  1. Java 代码执行。
1
2
3
4
5
shell复制代码$ java --add-modules  jdk.incubator.foreign -Dforeign.restricted=permit JEP389.java
WARNING: Using incubator modules: jdk.incubator.foreign
警告: 使用 incubating 模块: jdk.incubator.foreign
1 个警告
hello www.wdbyte.com

扩展阅读: JEP 389:外部链接器 API(孵化器)

  1. JEP 390:基于值的类的警告

添加了一个注解,用于标识当前是是基于值的类,比如 Java 8 引入的预防空指针的 Optional 类,现在已经添加了注解标识。

1
2
3
4
java复制代码@jdk.internal.ValueBased
public final class Optional<T> {
// ...
}

扩展阅读:基于值的类的警告

  1. JEP 392:打包工具

在 Java 14 中,JEP 343 引入了打包工具,命令是 jpackage,在 Java 14 新功能文章里也做了介绍:

使用 jpackage 命令可以把 JAR 包打包成不同操作系统支持的软件格式。

1
2
3
> css复制代码jpackage --name myapp --input lib --main-jar main.jar --main-class myapp.Main
>
>

常见平台格式如下:

  1. Linux: deb and rpm
  2. macOS: pkg and dmg
  3. Windows: msi and exe

要注意的是,jpackage 不支持交叉编译,也就是说在 windows 平台上是不能打包成 macOS 或者 Linux 系统的软件格式的。

在 Java 15 中,继续孵化,现在在 Java 16 中,终于成为了正式功能。

下面是一个例子,把一个简单的 Java Swing 程序打包成当前操作系统支持的软件格式,然后安装到当前电脑。

编写 Java 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码import javax.swing.*;
import java.awt.*;

public class JEP392 {

public static void main(String[] args) {
JFrame frame = new JFrame("Hello World Java Swing");
frame.setMinimumSize(new Dimension(800, 600));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JLabel lblText = new JLabel("Hello World!", SwingConstants.CENTER);
frame.getContentPane().add(lblText);
frame.pack();
frame.setVisible(true);

}
}

编译后,创建一个 JAR 文件。

1
2
3
shell复制代码$ javac JEP392.java
$ java JEP392.java
$ jar cvf JEP392.jar JEP392.class

将生成的 JEP392.jar 打包到符合当前平台的软件包中。

1
2
3
4
5
6
shell复制代码$ ~/develop/jdk-16.0.1.jdk/Contents/Home/bin/jpackage -i . -n JEP392 --main-jar hello.jar --main-class JEP392
$ ll
-rw-r--r--@ 1 darcy staff 50M 10 28 20:34 JEP392-1.0.dmg
-rw-r--r-- 1 darcy staff 864B 10 28 20:22 JEP392.class
-rw-r--r-- 1 darcy staff 1.0K 10 28 20:30 JEP392.jar
-rw-r--r-- 1 darcy staff 588B 10 28 20:22 JEP392.java

ll 后显示的 JEP392-1.0.dmg(我用的 MacOS ,所以格式是 dmg)就是打包后的结果。

双击这个文件后可以像 mac 软件一样安装。其他平台类似。

安装Java软件

安装后可以在启动台启动。

启动测试

不同的系统安装位置不同:

  • Linux: /opt
  • MacOS : /Applications
  • Windows: C:\Program Files\

扩展阅读:JEP 392:打包工具

  1. JEP 393:外部内存访问(第三次孵化)

此提案旨在引入新的 API 以允许 Java 程序安全有效的访问 Java 堆之外的内存。相关提案早在 Java 14 的时候就已经提出了,在 Java 15 中重新孵化,现在在 Java 16 中再次孵化。

此提案的目标如下:

  1. 通用:单个 API 应该能够对各种外部内存(如本机内存、持久内存、堆内存等)进行操作。
  2. 安全:无论操作何种内存,API 都不应该破坏 JVM 的安全性。
  3. 控制:可以自由的选择如何释放内存(显式、隐式等)。
  4. 可用:如果需要访问外部内存,API 应该是 sun.misc.Unsafa.

扩展阅读:外部内存访问

  1. JEP 394:instanceof 模式匹配

改进 instanceof 在 Java 14 中已经提出,在 Java 15 中继续预览,而现在,在 Java 16 中成为正式功能。

在之前,使用 instanceof 需要如下操作:

1
2
3
4
java复制代码if (obj instanceof String) {
String s = (String) obj; // grr...
...
}

多余的类型强制转换,而现在:

1
2
3
4
java复制代码if (obj instanceof String s) {
// Let pattern matching do the work!
...
}

扩展阅读:Java 14 新特性介绍 - instanceof

  1. JEP 395:Records

Record 成为 Java 16 的正式功能,下面是介绍 Java 14 时关于 Record 的介绍。

record 是一种全新的类型,它本质上是一个 final 类,同时所有的属性都是 final 修饰,它会自动编译出 public get hashcode 、equals、toString 等方法,减少了代码编写量。

示例:编写一个 Dog record 类,定义 name 和 age 属性。

1
2
3
4
java复制代码package com.wdbyte;

public record Dog(String name, Integer age) {
}

Record 的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码package com.wdbyte;

public class Java14Record {

public static void main(String[] args) {
Dog dog1 = new Dog("牧羊犬", 1);
Dog dog2 = new Dog("田园犬", 2);
Dog dog3 = new Dog("哈士奇", 3);
System.out.println(dog1);
System.out.println(dog2);
System.out.println(dog3);
}
}

输出结果:

1
2
3
shell复制代码Dog[name=牧羊犬, age=1]
Dog[name=田园犬, age=2]
Dog[name=哈士奇, age=3]

这个功能在 Java 15 中进行二次预览,在 Java 16 中正式发布。

  1. JEP 396:默认强封装JDK内部

Java 9 JEP 261引入了 --illegal-access 控制内部 API 访问和 JDK 打包的选项。

此 JEP 将 --illegal-access 选项的默认模式从允许更改为拒绝。通过此更改,JDK的内部包和 API(关键内部 API除外)将不再默认打开。

该 JEP 的动机是阻止第三方库、框架和工具使用 JDK 的内部 API 和包,增加了安全性。

  1. JEP 397:Sealed Classes(密封类)预览

Sealed Classes 再次预览,在 Java 15 新特性介绍文章里已经介绍过相关功能,并且给出了详细的使用演示,这里不再重复介绍。

下面是一段引用:

我们都知道,在 Java 中如果想让一个类不能被继承和修改,这时我们应该使用 final 关键字对类进行修饰。不过这种要么可以继承,要么不能继承的机制不够灵活,有些时候我们可能想让某个类可以被某些类型继承,但是又不能随意继承,是做不到的。Java 15 尝试解决这个问题,引入了 sealed 类,被 sealed 修饰的类可以指定子类。这样这个类就只能被指定的类继承。

而且 sealed 修饰的类的机制具有传递性,它的子类必须使用指定的关键字进行修饰,且只能是 final 、sealed 、non-sealed 三者之一。

扩展阅读:Java 15 新特性介绍

参考

  1. openjdk.java.net/projects/jd…
  2. docs.oracle.com/en/java/jav…

<完>

Hello world : ) 我是阿朗,一线技术工具人,认认真真写文章。

点赞的个个都是人才,不仅长得帅气好看,说话还好听。

文章持续更新,可以关注公众号「 程序猿阿朗 」或访问「程序猿阿朗博客](www.wdbyte.com) 」。

回复【资料】有我准备的各系列知识点和必看书籍。

本文 Github.com/niumoo/Java… 已经收录,有很多知识点和系列文章,欢迎Star。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java内存分配原理精讲 Java内存分配中的栈 Java内

发表于 2021-11-01

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」。

Java内存分配与管理是Java的核心技术之一,之前我们曾介绍过Java的内存管理与内存泄露以及Java垃圾回收方面的知识,今天我们再次深入Java核心,详细介绍一下Java在内存分配方面的知识。一般Java在内存分配时会涉及到以下区域:

◆寄存器:我们在程序中无法控制

◆栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中

◆堆:存放用new产生的数据

◆静态域:存放在对象中用static定义的静态成员

◆常量池:存放常量

◆非RAM存储:硬盘等永久存储空间

Java内存分配中的栈

在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。

当在一段代码块定义一个变量时,Java就在栈中 为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

Java内存分配中的堆

堆内存用来存放由new创建的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。 引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。引用变量就相当于是为数组或者对象起的一个名称。

引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序 运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。这也是 Java 比较占内存的原因。

实际上,栈中的变量指向堆内存中的变量,这就是Java中的指针!

常量池 (constant pool)

常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如:

◆类和接口的全限定名;

◆字段的名称和描述符;

◆方法和名称和描述符。

虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和 floating point常量)和对其他类型,字段和方法的符号引用。

对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的,对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引用。说到这里,对常量池中的字符串值的存储位置应该有一个比较明了的理解了。

在程序执行的时候,常量池 会储存在Method Area,而不是堆中。

堆与栈

Java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、 anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量数据(int, short, long, byte, float, double, boolean, char)和对象句柄(引用)。

栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:

int a = 3; int b = 3; 编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。

这时,如果再令 a=4;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响 到b的值。

要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。

String是一个特殊的包装类数据。可以用:

String str = new String(“abc”); String str = “abc”; 两种的形式来创建,第一种是用new()来新建对象的,它会在存放于堆中。每调用一次就会创建一个新的对象。而第二种是先在栈中创建一个对String类的对象引用变量str,然后通过符号引用去字符串常量池 里找有没有”abc”,如果没有,则将”abc”存放进字符串常量池,并令str指向”abc”,如果已经有”abc” 则直接令str指向“abc”。

比较类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==,下面用例子说明上面的理论。

String str1 = “abc”; String str2 = “abc”; System.out.println(str1==str2); //true 可以看出str1和str2是指向同一个对象的。

String str1 =new String (“abc”); String str2 =new String (“abc”); System.out.println(str1==str2); // false 用new的方式是生成不同的对象。每一次生成一个。

因此用第二种方式创建多个”abc”字符串,在内存中 其实只存在一个对象而已. 这种写法有利与节省内存空间. 同时它可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String(“abc”);的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。

另 一方面, 要注意: 我们在使用诸如String str = “abc”;的格式定义类时,总是想当然地认为,创建了String类的对象str。担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。只有通过new()方法才能保证每次都创建一个新的对象。

由于String类的immutable性质,当String变量需要经常变换 其值时,应该考虑使用StringBuffer类,以提高程序效率。

\

  1. 首先String不属于8种基本数据类型,String是一个对象。因为对象的默认值是null,所以String的默认值也是null;但它又是一种特殊的对象,有其它对象没有的一些特性。


2. new String()和new String(””)都是申明一个新的空字符串,是空串不是null;


3. String str=”kvill”;String str=new String (”kvill”)的区别

示例:

1
2
3
4
5
ini复制代码String s0="kvill"; 
String s1="kvill";
String s2="kv" + "ill";
System.out.println( s0==s1 );
System.out.println( s0==s2 );

结果为:

true

true

首先,我们要知结果为道Java 会确保一个字符串常量只有一个拷贝。

因为例子中的 s0和s1中的”kvill”都是字符串常量,它们在编译期就被确定了,所以s0==s1为true;而”kv”和”ill”也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,所以s2也是常量池中” kvill”的一个引用。所以我们得出s0==s1==s2;用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。

示例:

1
2
3
4
5
6
ini复制代码String s0="kvill"; 
String s1=new String("kvill");
String s2="kv" + new String("ill");
System.out.println( s0==s1 );
System.out.println( s0==s2 );
System.out.println( s1==s2 );

结果为:

false

false

false

例2中s0还是常量池 中”kvill”的应用,s1因为无法在编译期确定,所以是运行时创建的新对象”kvill”的引用,s2因为有后半部分 new String(”ill”)所以也无法在编译期确定,所以也是一个新创建对象”kvill”的应用;明白了这些也就知道为何得出此结果了。

  1. String.intern():

再补充介绍一点:存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的 intern()方法就是扩充常量池的一个方法;当一个String实例str调用intern()方法时,Java 查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用;看示例就清楚了

示例:

1
2
3
4
5
6
7
8
9
10
ini复制代码String s0= "kvill"; 
String s1=new String("kvill");
String s2=new String("kvill");
System.out.println( s0==s1 );
System.out.println( "**********" );
s1.intern();
s2=s2.intern(); //把常量池中"kvill"的引用赋给s2
System.out.println( s0==s1);
System.out.println( s0==s1.intern() );
System.out.println( s0==s2 );

结果为:

false

false //虽然执行了s1.intern(),但它的返回值没有赋给s1

true //说明s1.intern()返回的是常量池中”kvill”的引用

true

最后我再破除一个错误的理解:有人说,“使用 String.intern() 方法则可以将一个 String 类的保存到一个全局 String 表中 ,如果具有相同值的 Unicode 字符串已经在这个表中,那么该方法返回表中已有字符串的地址,如果在表中没有相同值的字符串,则将自己的地址注册到表中”如果我把他说的这个全局的 String 表理解为常量池的话,他的最后一句话,”如果在表中没有相同值的字符串,则将自己的地址注册到表中”是错的:

示例:

1
2
3
4
5
ini复制代码String s1=new String("kvill"); 
String s2=s1.intern();
System.out.println( s1==s1.intern() );
System.out.println( s1+" "+s2 );
System.out.println( s2==s1.intern() );

结果:

false

kvill kvill

true

在这个类中我们没有声名一个”kvill”常量,所以常量池中一开始是没有”kvill”的,当我们调用s1.intern()后就在常量池中新添加了一 个”kvill”常量,原来的不在常量池中的”kvill”仍然存在,也就不是“将自己的地址注册到常量池中”了。

s1==s1.intern() 为false说明原来的”kvill”仍然存在;s2现在为常量池中”kvill”的地址,所以有s2==s1.intern()为true。

  1. 关于equals()和==:

这个对于String简单来说就是比较两字符串的Unicode序列是否相当,如果相等返回true;而==是 比较两字符串的地址是否相同,也就是是否是同一个字符串的引用。

  1. 关于String是不可变的

这一说又要说很多,大家只 要知道String的实例一旦生成就不会再改变了,比如说:String str=”kv”+”ill”+” “+”ans”; 就是有4个字符串常量,首先”kv”和”ill”生成了”kvill”存在内存中,然后”kvill”又和” ” 生成 “kvill “存在内存中,最后又和生成了”kvill ans”;并把这个字符串的地址赋给了str,就是因为String的”不可变”产生了很多临时变量,这也就是为什么建议用StringBuffer的原因了,因为StringBuffer是可改变的。

下面是一些String相关的常见问题:

String中的final用法和理解

1
2
3
4
5
dart复制代码final StringBuffer a = new StringBuffer("111"); 
final StringBuffer b = new StringBuffer("222"); 
a=b;//此句编译不通过 
final StringBuffer a = new StringBuffer("111"); 
a.append("222");// 编译通过

可见,final只对引用的”值”(即内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象 的变化,final是不负责的

String常量池问题的几个例子

下面是几个常见例子的比较分析和理解:

1
2
3
4
5
6
7
8
9
ini复制代码String a = "a1"; 
String b = "a" + 1;
System.out.println((a == b)); //result = true
String a = "atrue";
String b = "a" + "true";
System.out.println((a == b)); //result = true
String a = "a3.4";
String b = "a" + 3.4;
System.out.println((a == b)); //result = true

分析:JVM对于字符串常量的”+”号连接,将程序编译期,JVM就将常量字符串的”+”连接优化为连接后的值,拿”a” + 1来说,经编译器优化后在class中就已经是a1。在编译期其字符串常量的值就确定下来,故上面程序最终的结果都为true。

1
2
3
4
ini复制代码String a = "ab"; 
String bb = "b";
String b = "a" + bb;
System.out.println((a == b)); //result = false

分析:JVM对于字符串引用,由于在字符串的”+”连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即”a” + bb无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给b。所以上面程序的结果也就为false。

1
2
3
4
ini复制代码String a = "ab"; 
final String bb = "b";
String b = "a" + bb;
System.out.println((a == b)); //result = true

分析:和[3]中唯一不同的是bb字符串加了final修饰,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的”a” + bb和”a” + “b”效果是一样的。故上面程序的结果为true。

1
2
3
4
5
ini复制代码String a = "ab"; 
final String bb = getBB();
String b = "a" + bb;
System.out.println((a == b)); //result = false
private static String getBB() { return "b"; }

分析:JVM对于字符串引用bb,它的值在编译期无法确定,只有在程序运行期调用方法后,将方法的返回值和”a”来动态连接并分配地址为b,故上面程序的结果为false。

通过上面4个例子可以得出得知:

String s = “a” + “b” + “c”;

就等价于String s = “abc”;

String a = “a”;

String b = “b”;

String c = “c”;

String s = a + b + c;

这个就不一样了,最终结果等于:

1
2
3
ini复制代码StringBuffer temp = new StringBuffer(); 
temp.append(a).append(b).append(c);
String s = temp.toString();

由上面的分析结果,可就不难推断出String 采用连接运算符(+)效率低下原因分析,形如这样的代码:

1
2
3
4
5
6
7
8
typescript复制代码public class Test { 
public static void main(String args[]) {
String s = null;
for(int i = 0; i < 100; i++) {
s += "a";
}
}
}

每做一次 + 就产生个StringBuilder对象,然后append后就扔掉。下次循环再到达时重新产生个StringBuilder对象,然后 append 字符串,如此循环直至结束。如果我们直接采用 StringBuilder 对象进行 append 的话,我们可以节省 N - 1 次创建和销毁对象的时间。所以对于在循环中要进行字符串连接的应用,一般都是用StringBuffer或StringBulider对象来进行 append操作。

String对象的intern方法理解和分析:

1
2
3
4
5
6
7
8
9
typescript复制代码public class Test4 { 
private static String a = "ab";
public static void main(String[] args){
String s1 = "a"; String s2 = "b";
String s = s1 + s2;
System.out.println(s == a);//false
System.out.println(s.intern() == a);//true
}
}

这里用到Java里面是一个常量池的问题。对于s1+s2操作,其实是在堆里面重新创建了一个新的对象,s保存的是这个新对象在堆空间的的内容,所以s与a的值是不相等的。而当调用s.intern()方法,却可以返回s在常量池中的地址值,因为a的值存储在常量池中,故s.intern和a的值相等。

总结

栈中用来存放一些原始数据类型的局部变量数据和对象的引用(String,数组.对象等等)但不存放对象内容。堆中存放使用new关键字创建的对象.。

字符串是一个特殊包装类,其引用是存放在栈里的,而对象内容必须根据创建方式不同定(常量池和堆).有的是编译期就已经创建好,存放在字符串常 量池中,而有的是运行时才被创建.使用new关键字,存放在堆中。

​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

弄懂“三门问题”,成功概率翻倍,来用代码验证一下 三门问题

发表于 2021-11-01

看到一段关于“三门问题”的视频,第一感觉就是视频的结论有误。本想一笑了之,但看了评论,迷惑了:三门问题的答案到底是什么?

作为勤学好问的码农,不知道最终答案,还是很难受的,于是深入研究一下,发现”小丑竟然是自己“。如果你想挑战一下自己,可以先跳过推理和结论部分,自己先得出一个答案,然后再看看是否正确。

一条朋友圈

在花了一个小时,弄懂三门问题之后,发了一条这样的朋友圈:

image-20211031201436187

三门问题:有三扇门,其中一扇后面是汽车,另外两扇是山羊。当你选择一扇门后,主持人从另外两扇门中打开一扇有山羊的。那么,此时换门是否会增加获得汽车的概率?

第一次错:直觉,换与不换都是1/2的概率;差点止步于此,得出结论:都是骗人的。

第二次错:列举,(选1,去2,换)、(选2,去1,换)、(选3,去1,不换)、(选3、去2、不换),看似概率依旧是1/2。但这里犯了一个错误,没引入首选的概率,也就是后两种情况不能按1/4算,只能按1/6算。

第三次引入概率:1/3(选1,去2,换)、1/3(选2,去1,换)、1/6(1/3 * 1/2)(选3,去1,不换)、1/6(1/3 * 1/2)(选3、去2、不换),后两项合计只有1/3概率。

所以,三门问题的答案是:选择换。概率会从原来的1/3,变成2/3;

通过这个问题在想:有时候,坚持可能是错的,可能是主观判断,可能环境已经发生了变化;但有时候又要坚持,要坚持对答案的怀疑,不断寻找答案。

如果从底层逻辑来说就是:坚持动态的看待问题。也就是:士别三日当刮目相待。

发完这条朋友圈,感觉这个问题有必要通过程序实现一下,同时写篇文章分享出来,于是就有了这篇文章。

如果上面的分析没看懂,也没关系,下面就结合代码再分析实践一下。

三门问题

三门问题出自美国的电视游戏节目Let’s Make a Deal,问题名字来自该节目的主持人蒙提·霍尔(Monty Hall)。

问题场景:

参赛者会看见三扇关闭了的门,其中一扇的后面有一辆汽车,选中后面有车的那扇门可赢得该汽车,另外两扇门后面则各藏有一只山羊。当参赛者选定了一扇门,但未去开启它的时候,节目主持人开启剩下两扇门的其中一扇,露出其中一只山羊。主持人其后会问参赛者要不要换另一扇仍然关上的门。问题是:换另一扇门是否会增加参赛者赢得汽车的机率。

据说,90%的人都选择了不换。你的选择是什么呢?

概率分析

先看下图,存在汽车、山羊1、山羊2,三个门:

image-20211031203316735

选手选择三个门的概率都是三分之一,下面进行具体的假设:

  • 假设选手选择了山羊1,那么主持人打的门只能是山羊2,因为有汽车的门是不能打开的。这种情况发生的概率为:1/3(选手选择山羊1的概率)* 1(主持人的选择是确定的) = 1/3;此时如果换,则赢得汽车;
  • 假设选手选择了山羊2,那么主持人打的门只能是山羊1,因为有汽车的门是不能打开的。这种情况发生的概率为:1/3(选手选择山羊2的概率)* 1(主持人的选择是确定的) = 1/3;此时如果换,则赢得汽车;
  • 假设选手选择了汽车,那么主持人有两种打开选择:山羊1和山羊2。主持人选择山羊1的概率:1/3(选手选择汽车的概率)* 1/2(主持人二选一) = 1/6;主持人选择山羊2的概率:1/3(选手选择汽车的概率)* 1/2(主持人二选一) = 1/6;所以,当选手选择了汽车的门时,发生的概率为:1/3 * 1/2 + 1/3 * 1/2 = 1/3。此时如果不换,则赢得汽车;

很显然,三种情况发生的概率都为三分之一,换之后赢得汽车的概率是不换的2倍。也就是说:换之后,赢得汽车的概率变成了2/3。

程序演示

上面做了理论分析,下面写一段代码,来验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
ini复制代码public class ThreeDoors {
​
/**
* 随机选择器
*/
private static final Random RANDOM = new Random();
​
/**
* 成功总次数
*/
private static int SUCCESS_COUNT = 0;
​
/**
* 重复执行10w次
*/
private static final int PLAY_TIMES = 100000;
​
public static void main(String[] args) {
​
// 执行游戏10w次
for (int i = 0; i < PLAY_TIMES; i++) {
playGame();
}
​
// 计算选择"换"的概率
BigDecimal yield = new BigDecimal(SUCCESS_COUNT)
.divide(new BigDecimal(PLAY_TIMES), 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal(100));
System.out.println("执行" + PLAY_TIMES + "次实验,选择【交换】的概率为:" + yield + "%");
}
​
public static void playGame() {
​
// 初始化三扇门,默认为false,都没有车
boolean door1 = false, door2 = false, door3 = false;
​
// 选手选择的门是否为汽车,true:是
boolean pickedDoor;
// 最后剩下的门是否为汽车,true:是
boolean leftDoor;
​
// 第一步:随机选择一扇门,放入汽车
switch (pickDoor(3)) {
case 1:
door1 = true;
break;
case 2:
door2 = true;
break;
case 3:
door3 = true;
break;
default:
System.out.println("异常数值");
break;
}
​
// 第二步:选手选择一扇门,依旧采用上面选门的算法
int playerPickedDoor = pickDoor(3);
​
// 第三步:主持人移除一扇有山羊的门
// 其中主持人只能二选一,移除1一扇门,相当于选择了另一扇门
if (playerPickedDoor == 1) {
// 选手选择门1
pickedDoor = door1;
// 如果门2有车,则只能移除门3
if (door2) {
leftDoor = door2;
} else if (door3) {
// 如果门3有车,则只能移除门2
leftDoor = door3;
} else {
// 两个门都没车,随机二选一
if (pickDoor(2) == 1) {
leftDoor = door2;
} else {
leftDoor = door3;
}
}
} else if (playerPickedDoor == 2) {
// 选手选择门2
pickedDoor = door2;
// 如果门1有车,则只能移除门3
if (door1) {
leftDoor = door1;
} else if (door3) {
// 如果门3有车,则只能移除门1
leftDoor = door3;
} else {
// 两个门都没车,随机二选一
if (pickDoor(2) == 1) {
leftDoor = door1;
} else {
leftDoor = door3;
}
}
} else {
// 选手选择门3
pickedDoor = door3;
// 如果门1有车,则只能移除门2
if (door1) {
leftDoor = door1;
} else if (door2) {
// 如果门2有车,则只能移除门1
leftDoor = door2;
} else {
// 两个门都没车,随机二选一
if (pickDoor(2) == 1) {
leftDoor = door1;
} else {
leftDoor = door2;
}
}
}
​
// 第四步:上述结果一定的情况,选手选择更换门
pickedDoor = leftDoor;
​
// 第五步:判断该门是否有车
if (pickedDoor) {
SUCCESS_COUNT++;
}
}

/**
* 随机选择一个门
*/
public static int pickDoor(int bound) {
return RANDOM.nextInt(bound) + 1;
}
}

上述实现方法,暂且未考虑算法优化,只是简单情况判断处理。

上述实现分以下几步:

  • 第一步:随机选择一扇门,放入汽车,这里采用Random随机数,如果对应的门后为车,则对应的值设置为true;
  • 第二步:选手选择一扇门,算法依旧采用Random随机数;
  • 第三步:在选手选择一扇门的前提下,主持人移除一扇没有汽车的门。这里并未处理移除的门,而是记录了移除之后剩下的那扇门的值。如果两扇门都没有车,则随机二选一。
  • 第四步:选手选择交换,即选手选择的门变成了剩下的那扇门。
  • 第五步:开门,验证,如果成功记录一次;
  • 第六步:执行10w次之后,计算百分比;

最终打印日志如下:

1
erlang复制代码执行100000次实验,选择【交换】的概率为:66.7500%

多执行几次,会发现几乎都在66%-67%之间,说明选择【换】,的确可以让成功的概率翻倍。

小结

最后,回顾一下整个过程:无意看到一条讲”三门问题“的视频,先是做出了直观判断(错误的),对别人的结论嗤之以鼻,然后发现许、异议。于是,开始寻求佐证,最终得到了正确的答案。

正像在朋友圈中说的:有时候,坚持可能是错的,可能是因为主观判断,也可能是因为环境已经发生了变化;但有时候又要坚持,要坚持对答案的怀疑,对答案的不断追寻。

这也应该是我们做事的底层逻辑,不能单靠【感觉】来判断,更多的要采用事实作为依据。特别是程序员,我们还可以用程序来解决类似的问题。

同时,你是否发现,用程序来解决生活中的一些问题,不也是很有意思的吗?

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Ajax教程 1-HTTP格式简介 总目录结构 2-expr

发表于 2021-11-01

1-HTTP格式简介

1.1-http请求报文

请求行

1
ini复制代码POST  /s?ie=utf-8  HTTP/1.1  Host: atguigu.com/

包含请求方法(get , post ...),请求URL,http协议以及版本

请求头

1
2
3
4
5
makefile复制代码Host: atguigu.com
Cookie: name=guigu
Content-type: application/x-wiww-form-urlencoded
User-Agent: chrome 83
...

格式为“属性名:属性值”,服务端据此获取客户端的信息。与缓存相关的规则信息,均包含在header中

请求体

1
ini复制代码username=admin&password=admin

param1=value1&param2=value2...的格式,承载多个请求参数的数据

1.2-http响应报文

响应行:

1
复制代码HTTP/1.1 200 OK

①报文协议及版本

②状态码及状态描述

响应头:

1
2
3
4
yaml复制代码Content-Type: text/html; charset=utf-8
Content-length: 2048
Content-encoding: gzip
...

响应报文头,也是由多个属性组成,格式为“属性名:属性值”

响应体:

1
2
3
4
5
6
7
xml复制代码<html>
<head>
<body>
<h1>hello</h1>
<body>
</head>
</html>

即网页的html代码

总目录结构

以下所有的实验的目录结构如下

  • Ajax
  • 1-GET.html
  • 2-POST.html
  • 3-请求超时与网络异常处理.html
  • 4-请求取消.html
  • 5-取消重复发送请求.html
  • server.js

2-express基本使用

express是一个node.js Web 架構,链接( expressjs.com/ )

GET请求

使用npm下载express,命令行为:npm install express --save,创建Ajax文件夹,在里面创建1-GET.html和server.js

1-GET.html:

1
2
3
4
5
6
7
8
9
10
11
12
javascript复制代码    <style>
#result{
width: 200px;
height: 150px;
border: 2px solid rgb(245, 64, 230);
}
</style>

<body>
<button>get</button>
<div id="result"></div>
</body>

server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
javascript复制代码    // 1.引入express
const express = require('express');

// 2.创建应用对象
const app = express();

// 3.创建路由规则
// request是对请求报文的封装
// response 是对响应报文的封装
app.get('/server',(request,response) => {
// 设置响应头 允许跨域
response.setHeader('Access-Control-Allow-Origin','*')
// 设置响应体
response.send("HELLO AJAX")
})

// 4.监听端口启动服务
app.listen(8000,()=>{
console.log("服务已经启动,8000 端口监听中...");
})

在Ajax文件夹打开终端输入node server.js,终端显示 服务已经启动,8000 端口监听中… ,之后打开http://127.0.0.1:8000/server显示内容为:

HELLO AJAX

现在在1-GET.html里面添加script代码

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
javascript复制代码    <script>
function XH(){
var xhr = new XMLHttpRequest();
// 2.初始化,设置请求方法和url
xhr.open('GET','http://127.0.0.1:8000/server')
//3.发送
xhr.send();
// 4.事件绑定 处理服务端返回的结果
xhr.onreadystatechange = function(){
// readyState表示状态 0 1 2 3 4
// 0 UNSENT 代理被创建,但尚未调用 open() 方法。
// 1 OPENED open() 方法已经被调用。
// 2 HEADERS_RECEIVED send() 方法已经被调用,并且头部和状态已经可获得。
// 3 LOADING 下载中; responseText 属性已经包含部分数据。
// 4 DONE 下载操作已完成。
if(xhr.readyState === 4){
// 判断响应状态码(如 200 404 403 ...)
// 2xx表示成功
if(xhr.status >= 200 && xhr.status <300){
// 1.处理响应行
console.log(xhr.status);//状态码
console.log(xhr.statusText);//状态字符串
console.log(xhr.getAllResponseHeaders());//所有响应头
console.log(xhr.response);//响应体
}
}
}
}
</script>

另外给1-GET.html里面的button绑定点击事件

1
ini复制代码<button onclick="XH()">get</button>

在浏览器打开1-GET.html, 点击按钮, 控制台显示:

200

OK

content-length: 10

content-type: text/html; charset=utf-8

HELLO AJAX

GET请求的参数说明

那么在ajax如何请求参数呢?直接将xhr.open('GET','http://127.0.0.1:8000/server')
后面加上参数,即:xhr.open('GET','http://127.0.0.1:8000/server?a=100&b=200'),就会获得参数a:100,b:200

POST请求

接下来再新建2-POST.html,如下:

2-POST.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
javascript复制代码      <style>
#result{
width: 200px;
height: 100px;
border: solid 2px red;
}
</style>

<body>
<div id="result"></div>
</body>

<script>
const result = document.getElementById("result")
result.addEventListener("mouseover",function(){
const xhr = new XMLHttpRequest;
xhr.open("post","http://127.0.0.1:8000/server");
xhr.send();
xhr.onreadystatechange = function(){
result.innerHTML = xhr.response;
}
})
</script>

在server.js中新增加:

1
2
3
4
5
6
javascript复制代码    app.post('/server',(request,response) => {
// 设置响应头 允许跨域
response.setHeader('Access-Control-Allow-Origin','*')
// 设置响应体
response.send("HELLO AJAX POST")
})

在浏览器打开2-POST.html,当鼠标移动进去框内时显示:

HELLO AJAX POST

POST请求的参数说明

修改2-POST.html中的xhr.send:

1
2
3
4
5
6
7
javascript复制代码      // 请求体的参数发送在send方法里面,

// 方式一:
// xhr.send("a=100&b=200");

// 方式二:
xhr.send("a:100&b:200")

在post方式中请求参数在send方法中

设置请求头

在2-POST.html添加

1
arduino复制代码xhr.setRequestHeader("name","lihua")

设置请求头 name:lihua, 但是打开浏览器F12->network会看见报错,原因是会检测到头信息检验是否可用。而明显这个头信息不可用

为了解决这个问题,应该将server.js中的app.post改为app.all,而且需要再新设置一个响应头response.setHeader('Access-Control-Allow-Headers','*'),这样的话就可以自定义请求头

1
2
3
4
5
6
7
8
9
javascript复制代码    // 可以接受任意类型的请求
app.all('/server',(request,response) => {
// 设置响应头 允许跨域
response.setHeader('Access-Control-Allow-Origin','*')
// 设置响应头
response.setHeader('Access-Control-Allow-Headers','*')
// 设置响应体
response.send("HELLO AJAX POST")
})

.all方法可以接受任意类型的请求

服务端响应JSON数据

修改server.js中的app.all

1
2
3
4
5
6
7
8
9
10
11
12
javascript复制代码    app.all('/server',(request,response) => {
// 设置响应头 允许跨域
response.setHeader('Access-Control-Allow-Origin','*')
// 设置响应头
response.setHeader('Access-Control-Allow-Headers','*')
const data = {
like:'football'
}
let str = JSON.stringify(data)
// 设置响应体
response.send(str)
})

添加类data,JSON.stringify(data)将类转化为JSON数据,并作为响应体发送

此时打开2-POST.html在浏览器运行中,结果为

{“like”:”football”}

为了使JSON数据转化为类数据,添加xhr.responseType = 'json',将显示结果改为:
result.innerHTML = xhr.response.like则结果为

football

也可以用JSON.parse转换为类数据

3-nodemon

使用npm install -g nodemon下载nodemon,

链接 [ www.npmjs.com/package/nod… ]

nodemon的作用是服务端直接保存代码时不用重新启动服务,服务端自动更新服务

使用nodemon server.js开启服务,修改server.js的代码

1
2
3
kotlin复制代码  const data = {
like:'football-game'
}

保存,服务端自动更新,浏览器运行2-POST.html的结果为

football-game

4-问题解决

4.1-请求超时与网络异常处理

添加页面3-请求超时与网络异常处理.html

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
javascript复制代码    <style>
#result{
width: 200px;
height: 100px;
border: red 2px solid;
}
</style>

<body>
<button>点击发送请求</button>
<div id="result"></div>

</body>

<script>
const btn = document.getElementsByTagName('button')[0];
const result = document.getElementById('result');
btn.addEventListener('click',function(){
const xhr = new XMLHttpRequest();
xhr.open('get','http://127.0.0.1:8000/delay')
xhr.send();


// 设置超时时间
xhr.timeout = 2000
// 设置超时回调
xhr.ontimeout = function(){
alert("请求超时,请稍后重试");
}
// 设置网络异常回调
xhr.onerror = function(){
alert("网络异常,请检查您的网络")
}


xhr.onreadystatechange = function(){
if(xhr.readyState === 4){
if(xhr.status >= 200 && xhr.status <300){
result.innerHTML = xhr.response;
}
}
}
})
</script>

server.js新增代码

1
2
3
4
5
6
7
javascript复制代码    app.get('/delay',(request,response) => {
// 设置响应头 允许跨域
response.setHeader('Access-Control-Allow-Origin','*')
setTimeout(() => {
response.send('延时响应')
}, 3000);
})

新增了链接/delay, server.js中设置定时器为3s后响应,由于xhr.timeout = 2000,则响应时间超时,弹出弹窗"请求超时,请稍后重试",若网络异常,弹出弹窗"网络异常,请检查您的网络"

4.2-Ajax取消请求

新增加页面4-请求取消.html

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
javascript复制代码    <body>
<button>请求发送</button>
<button>请求取消</button>
</body>

<script>
const btn1 = document.getElementsByTagName("button")[0];
const btn2 = document.getElementsByTagName("button")[1];
let x;
btn1.addEventListener('click',function(){
x = new XMLHttpRequest();
x.open('get',"http://127.0.0.1:8000/delay");
x.send();
x.onreadystatechange = function(){
if(x.readyState === 4){
if(x.status >= 200 && x.status <300){
console.log("请求成功");
}
}
}
})
btn2.onclick = function(){
x.abort();
}
</script>

按下btn1则请求发送,但是有3s延迟,在此期间按下btn2则请求取消,.abort()用于取消Ajax的请求发送

4.3-解决重复发送请求问题

新增加页面5-取消重复发送请求.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
javascript复制代码    <body>
<button>发送请求</button>
</body>

<script>
const btn = document.getElementsByTagName("button")[0];
let xhr;

issending = false// 标识变量 是否正在请求Ajax

btn.addEventListener('click',function(){
if(issending == true) xhr.abort();// 如果状态码为发送状态(true),则取消发送请求
issending = true;// 此时为正在发送
xhr = new XMLHttpRequest();
xhr.open('get','http://127.0.0.1:8000/delay');
xhr.send();
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){
issending = false;//发送完毕,状态码改为不在发送状态
}
}
})
</script>

在某种情况下(如:网络速度慢)用户会一直点击相同的按钮,由于连续点击按钮会持续地发送Ajax请求,当请求的速度慢或者网络卡时,会导致大量相同请求同时存在,对服务端产生压力,因此需要解决这个问题。通过设置状态码的办法,当第一次请求不成功而且第二次点击按钮的情况下,取消第一次的请求并响应第二次的请求。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

RedisTemplate中的execute方法如何使用

发表于 2021-11-01

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

温故才能知新(复习一下)

上篇文章我们学习了RedisTemplate中的execute的概念,理解了execute的概念。

execute提供了基础的操作Redis方法的实现,为其他特定数据类型操作方法提供了基础方法的实现。

今天我们是要来学习一下RedisTemplate中的execute方法具体使用方法,并且提供出相应的代码实现,一起来学习吧。

如何正确使用execute方法

execute(RedisCallback action)

1
r复制代码public <T> T execute(RedisCallback<T> action)

此方法需要传入的参数时一个RedisCallback对象实例,实际是一个Redis连接成功的回调方法,一般都是以下方式去调用的。

通过Redis的链接来操作Redis数据库中的键值。

代码使用:

1
2
3
arduino复制代码redisTemplate.execute((connection) -> {
return connection.del(new byte[][]{"redis-key"});
});

execute(RedisCallback action, boolean exposeConnection)

1
java复制代码public <T> T execute(RedisCallback<T> action, boolean exposeConnection)

大家可以看得出来,这个方法同上面的方法,只是多了一个exposeConnection的布尔类型参数,那么这个参数是干什么的?有什么作用呢?

exposeConnection参数的含义是是否要暴露connection,如果为true,那么就可以在回调函数中使用当前连接connection对象。

代码使用:

1
2
3
arduino复制代码redisTemplate.execute((connection) -> {
return connection.del(new byte[][]{"redis-key"});
}, true);

execute(RedisCallback action, boolean exposeConnection, boolean pipeline)

1
java复制代码public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline)

如果你看了这几个方法的源码,你就会发现,前面三个execute方法的核心其实就是这个,因为前两个方法依然会调用这个方法,只不过多了两个参数值而已。

exposeConnection我们已经说过了,pipeline的含义则是是否开启管道,管道是可以承载一定数据的链路。

代码使用:

1
2
3
arduino复制代码redisTemplate.execute((connection) -> {
return connection.del(new byte[][]{"redis-key"});
}, true, false);

execute(SessionCallback session)

1
r复制代码public <T> T execute(SessionCallback<T> session)

这个方法有所不同,因为参数和前三个参数不一样了,之前的回调函数是RedisCallback,这个方法的回调是SessionCallback回调对象。

是可以传入SessionCallback回调对象的方法。

注意

这里要注意一点,虽然execute方法是底层方法,但是并不代表一定要用,我们在日常开发中其实最好还是使用基于execute方法封装的一些特定数据类型的操作Redis方法,因为execute方法使用起来并没有其他的方法简单,反而更容易出错。

小结

今天学习了RedisTemplate的execute相关的几个重载方法是如何使用的,不知道大家有什么样的心得呢?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Security专栏(Security 中的权

发表于 2021-10-31

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

写在前面

通过前面几讲的介绍,相信你已经对 Spring Security 中的认证流程有了更全面的了解。认证是实现授权的前提和基础 。

通常我们在执行授权操作时需要明确目标用户,只有明确目标用户才能明确它所具备的角色和权限,用户、角色和权限也是 Spring Security 中所采用的授权模型,所以今天我们来学习Security中的权限和角色。

多说一句 欢迎大家点击我头像查看Security专栏,设计模式专栏已完结。

Spring Security 中的权限和角色

我们可以回顾下,配置方法的处理过程位于 WebSecurityConfigurerAdapter 类中,但使用的是另一个 configure(HttpSecurity http) 方法,示例代码如下所示:

1
2
3
4
5
6
7
8
java复制代码protected void configure(HttpSecurity http) throws Exception {
        http
           .authorizeRequests().anyRequest().authenticated()
           .and()
           .formLogin()
.and()
           .httpBasic();
}

这是 Spring Security 中作用于访问授权的默认实现方法。

基于权限进行访问控制

image.png

上图中的 GrantedAuthority 对象代表的就是一种权限对象,而一个 UserDetails 对象具备一个或多个 GrantedAuthority 对象。通过这种关联关系,实际上我们就可以对用户的权限做一些限制,如下所示:

image.png

如果用代码来表示这种关联关系,可以采用如下所示的实现方法:

1
2
3
4
java复制代码UserDetails user = User.withUsername("yn")
     .password("123456")
     .authorities("create", "delete")
     .build();

可以看到,这里我们创建了一个名为“jianxiang”的用户,该用户具有“create”和“delete”这两个权限。在 Spring Security 中,提供了一组针对 GrantedAuthority 的配置方法。例如:

  • hasAuthority(String),允许具有特定权限的用户进行访问;
  • hasAnyAuthority(String),允许具有任一权限的用户进行访问。

可以使用上述两个方法来判断用户是否具备对应的访问权限,我们在WebSecurityConfigurerAdapter 的 configure 方法中添加如下代码:

1
2
3
4
5
java复制代码@Override
protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic();
http.authorizeRequests().anyRequest().hasAuthority("CREATE");       
}

这段代码的作用是对于任何请求,只有权限为“CREATE”才能采用访问。如果我们修改一下代码:

1
java复制代码http.authorizeRequests().anyRequest().hasAnyAuthority("CREATE", "DELETE");

此时,只要具备“CREATE”和“DELETE”中任意一种权限的用户都能进行访问。

这两个方法实现起来都比较简单,但局限性也很大,因为我们无法基于一些来自环境和业务的参数灵活控制访问规则。为此,Spring Security 还提供了一个 access() 方法,该方法允许开发人员传入一个表达式进行更加细粒度的权限控制。

这里,我们将引入 SpEL,它是 Spring Expression Language 的简称,是 Spring 框架提供的一种动态表达式语言。基于 SpEL,只要该表达式的返回值是 true,access() 方法就会允许用户访问。如下示例:

1
java复制代码http.authorizeRequests().anyRequest().access("hasAuthority('CREATE')");

上述代码与使用 hasAuthority() 方法的效果是完全一致的,但如果是更为复杂的场景,access() 方法的优势就很明显了。我们可以灵活创建一个表达式,然后通过 access() 方法确定最后的结果,示例代码如下所示:

1
2
java复制代码String expression = "hasAuthority('CREATE') and !hasAuthority('Retrieve')"; 
http.authorizeRequests().anyRequest().access(expression);

上述代码的效果是只有拥有“CREATE”权限且不拥有“Retrieve”权限的用户才能进行访问。

基于角色进行访问控制

讨论完权限,我们再来看角色,你可以把角色看成是拥有多个权限的一种数据载体,如下图所示,这里我们分别定义了两个不同的角色“User”和“Admin”,它们拥有不同的权限:

image.png

讲到这里,你可能会认为 Spring Security 应该提供了一个独立的数据结构来承载角色的含义。但事实上,在 Spring Security 中,并没有定义类似“GrantedRole”这种专门用来定义用户角色的对象,而是复用了 GrantedAuthority 对象。事实上,以“ROLE_”为前缀的 GrantedAuthority 就代表了一种角色,因此我们可以使用如下方式初始化用户的角色:

1
2
3
4
java复制代码UserDetails user = User.withUsername("yn")
      .password("123456")
      .authorities("ROLE_ADMIN")
      .build();

上述代码相当于为用户“jianxiang”指定了“ADMIN”这个角色。为了给开发人员提供更好的开发体验,Spring Security 还提供了另一种简化的方法来指定用户的角色,如下所示

1
2
3
4
java复制代码UserDetails user = User.withUsername("yn")
      .password("123456")
      .roles("ADMIN")
      .build();

和权限配置一样,Spring Security 也通过使用对应的 hasRole() 和 hasAnyRole() 方法来判断用户是否具有某个角色或某些角色,使用方法如下所示:

1
java复制代码http.authorizeRequests().anyRequest().hasRole("ADMIN");

当然,针对角色,我们也可以使用 access() 方法完成更为复杂的访问控制。而 Spring Security 还提供了其他很多有用的控制方法供开发人员进行灵活使用。作为总结,下表展示了常见的配置方法及其作用:

配置方法 作用
anonymous() 允许匿名访问
authenticated() 允许认证用户访问
denyAll() 无条件禁止一切访问
hasAnyAuthority(String) 允许具有任一权限的用户进行访问
hasAnyRole(String) 允许具有任一角色的用户进行访问
hasAuthority(String) 允许具有特定权限的用户进行访问
hasIpAddress(String) 允许来自特定 IP 地址的用户进行访问
hasRole(String) 允许具有特定角色的用户进行访问
permitAll() 无条件允许一切访问

Spring Security 中的配置方法列表

好了,今天Security 中的权限和角色就讲到这里。

总结

这一讲我们关注的是对请求访问进行授权,而这个过程需要明确 Spring Security 中的用户、权限和角色之间的关联关系。一旦我们对某个用户设置了对应的权限和角色,那么就可以通过各种配置方法来有效控制访问权限。下期我们讲权限的匹配器,下期见,加油。

弦外之音

感谢你的阅读,如果你感觉学到了东西,麻烦您点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

聊聊不一样的策略模式(值得收藏)

copy对象,这个操作有点骚!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Java实用技术】字符串的拆分怎么最快?

发表于 2021-10-31
  • 小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
  • 本文同时参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

本文是《【Java实用技术】字符串的拆分用什么方法好?》的姊妹篇,拆分方法最常用,使用看似简单但也有很多复杂的细节要注意,今天我们就来聊一聊字符串拆分怎么最快?

2种拆分操作性能对比

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
java复制代码import org.apache.commons.lang3.StringUtils;

/**
 * Java实用技术课程 By Pandas.
 * 公众号:Java实用技术手册
 * JDK版本:jdk1.8.0_66
 *
 * @author Pandas
 * @date 2021/10/31
 */
public class StringSplitQuick {

    public static void main(String[] args) {

        StringBuilder sb = new StringBuilder();
        int max = 100_0000;
        for (int i = 0; i < max; i++) {
            sb.append("a.");
        }
        // 构造一个100W个"a."拼接成的字符串:"a.a.a.a.a.......a."
        String str = sb.toString();

        long start1 = System.currentTimeMillis();
        String[] cs1 = str.split("\.");
        long cost1 = System.currentTimeMillis() - start1;

        long start2 = System.currentTimeMillis();
        String[] cs2 = StringUtils.split(str, '.');
        long cost2 = System.currentTimeMillis() - start2;

        long start11 = System.currentTimeMillis();
        String[] cs11 = str.split("a");
        long cost11 = System.currentTimeMillis() - start11;

        long start22 = System.currentTimeMillis();
        String[] cs22 = StringUtils.split(str, 'a');
        long cost22 = System.currentTimeMillis() - start22;

        System.out.println("原生拆分.耗时:" + cost1 + "ms");
        System.out.println("StringUtils拆分.耗时:" + cost2 + "ms");
        System.out.println("原生拆分a耗时:" + cost11 + "ms");
        System.out.println("StringUtils拆分a耗时:" + cost22 + "ms");
    }
}

运行结果如下:

image-20211031214730805

image-20211031214730805

总体上来说,原生方法拆分是比StringUtils拆分耗时久点。

还能不能再快呢?这就要分析下源码。

字符串拆分之arraycopy

  • 对于字符串拆分的原生操作

原生方法源码

  • StringUtils拆分操作

StringUtils源码

我们注意到上面2个方法都用到了list.toArray()方法,这个方法的源码用到了System.arraycopy(elementData, 0, a, 0, size);方法,这个操作不仅增加额外耗时,也增加内存消耗。

PS:List扩容过程中也会用到这System.arraycopy()方法哦,有兴趣的同学可以多看看他的实现。

1
2
3
4
5
6
7
8
9
java复制代码public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

如果内存不够的话,一开始的例子,你可能会遇到下面的错误:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

考虑到我们在通常开发场景中,我们使用字符串拆分后的操作一般是对拆分后内容遍历,因此使用数组还是List,都不影响我们操作。

上述方法中的List转数组,白白消耗了内存和时间。

在有的工具类(比如hutool)中就改成了List<String>作为返回类型。

补充说明

在《阿里巴巴Java开发手册》(嵩山版)中,有这么一句话:

阿里巴巴Java开发手册

在上一节中,无论是原生split还是StringUtils,都将拆分后为空字符串的部分去掉,因此原始的List容量实际是小于等于保留全部分割数据的大小的。这个在一些拆分后业务处理是有影响的=>就是会数组越界。

比如很常见的场景,由"key=value"组成的字符串,按照“=”拆分,如果不注意,直接使用arr[1]获取value,一定会数组越界。

这时候使用StringUtils.splitPreserveAllTokens(str, "=");可以有效保留空白字符位置。

当然,如果字符串中连要分割的字符都没有,上述方法还是不能返回第二个位置的数据。

这个时候你需要自己写一个字符串拆分工具类,返回固定长度的数组/List,这样方法调用者可以放心使用数组。

感谢阅读本期内容,希望对新入行的你有帮助。

往期内容:

  • 我决定写一本Java实用技术,特点实用!实用!还是实用!
  • 【Java实用技术】必备字符串操作之判空
  • 【Java实用技术】java中关于整数的几个冷知识,总有一个你不知道
  • 【Java实用技术】字符串的截取用什么方法好?
  • 【Java实用技术】字符串的拆分用什么方法好?有一半程序员都掉过split坑

我是Pandas,专注Java实用技术分享,公众号Java实用技术手册和B站均有视频解说,欢迎来玩。

如果你觉得这篇文章有用,别忘了点赞+关注,一起进步!

img

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

jdbc和mybatis的流式查询使用 导语:

发表于 2021-10-31

导语:

有些时候我们所需要查询的数据量比较大,但是jvm内存又是有限制的,数据量过大会导致内存溢出。这个时候就可以使用流式查询,
数据一条条的返回,处理完一条在拿下一条数据,这样每次在内存里面的数据其实很小,不会导致内存溢出。
本文里面会讲到jdbc的流式查询和mybatis的流式查询。

jdbc流式查询:

jdbc的流式查询需要在生成PreparedStatement的时候设置三个参数。如下:

1
2
ini复制代码PreparedStatement stmt = jdbcTemplate.getDataSource().getConnection().prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(Integer.MIN_VALUE);

主要使用到的是java.sql.Connection的prepareStatement方法。

1
arduino复制代码PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException;

resultSetType和resultSetConcurrency我们要分别设置为ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY。
还有就是fetchSize设置为Integer.MIN_VALUE,一开始比较疑惑为啥是这个值。后来发现代码里面对这个值其实是有特殊处理的。
这个是com.mysql.cj.jdbc.StatementImpl的setFetchSize方法。

1
2
3
4
5
6
7
8
9
10
java复制代码@Override
public void setFetchSize(int rows) throws SQLException {
synchronized (checkClosed().getConnectionMutex()) {
if (((rows < 0) && (rows != Integer.MIN_VALUE)) || ((this.maxRows > 0) && (rows > this.getMaxRows()))) {
throw SQLError.createSQLException(Messages.getString("Statement.7"), MysqlErrorNumbers.SQL_STATE_ILLcom.mysql.cj.jdbc.StatementImpl的方法EGAL_ARGUMENT, getExceptionInterceptor());
}

this.query.setResultFetchSize(rows);
}
}

resultSetType,有以下三种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
php复制代码/**
* The constant indicating the type for a <code>ResultSet</code> object
* whose cursor may move only forward.
* @since 1.2
*/
int TYPE_FORWARD_ONLY = 1003;

/**
* The constant indicating the type for a <code>ResultSet</code> object
* that is scrollable but generally not sensitive to changes to the data
* that underlies the <code>ResultSet</code>.
* @since 1.2
*/
int TYPE_SCROLL_INSENSITIVE = 1004;

/**
* The constant indicating the type for a <code>ResultSet</code> object
* that is scrollable and generally sensitive to changes to the data
* that underlies the <code>ResultSet</code>.
* @since 1.2
*/
int TYPE_SCROLL_SENSITIVE = 1005;
1
2
ini复制代码stmt = conn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(Integer.MIN_VALUE);

resultSetConcurrency有以下两种,流式查询要设置为只读的,数据不会被更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
php复制代码/**
* The constant indicating the concurrency mode for a
* <code>ResultSet</code> object that may NOT be updated.
* @since 1.2
*/
int CONCUR_READ_ONLY = 1007;

/**
* The constant indicating the concurrency mode for a
* <code>ResultSet</code> object that may be updated.
* @since 1.2
*/
int CONCUR_UPDATABLE = 1008;

mybatis流式查询:

mapper中的代码:

1
2
3
4
less复制代码@Select("select * from xxx order by xx desc")
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)
@ResultType(XxxObject.class)
void queryStreamResult(ResultHandler<XxxObject> handler);

在查询方法上加入注解@Options和@ResultType。设置参数不用多说和上面jdbc的底层是一样的,参数值也一样。
只不过设置了@ResultType告诉程序应该要返回什么类型的对象。还有这个ResultHandler其实就是Consumer的函数式接口用来处理每一条
返回的数据。

具体方法中的代码:

1
2
3
4
5
6
7
typescript复制代码@Override
public Boolean dealDataList() {
mapper.queryStreamResult(resultContext -> {
dealSingleData(resultContext.getResultObject().getUid());
});
return true;
}

这里怎么使用每一条返回的数据只要在resultContext使用ResultObject就可以拿到上面mapper设置的XxxObject对象进行操作了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL的表连接

发表于 2021-10-31

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

  1. 常见的表连接方式

MySQL中表的连接方式主要分为内连接和外连接,其中外连接又可以分为左外连接和右外连接

  • inner join:内连接,结果取连接两两个表数据的交集
  • left [outer] join:左外连接,取两表数据的交集以及左表中独有的数据
  • right [outer] join:右外连接,取两表数据的交集以及右表中的独有数据
  1. INNER JOIN

inner join内连接是根据指定的连接条件,获取两个表的数据交集。

2.1 内连接查询流程

  1. 首先确定驱动表和被驱动表,mysql会根据内部机制判定结果集小的为驱动表,而不是按照sql的书写顺序,可以使用explain命令查看执行计划中的表顺序
  2. 内连接联合查询时,取出驱动表的一条顺序,然后根据连接条件遍历被驱动表种的所有数据
  3. 如果被驱动表中有数据满足连接条件,则取出当前数据作为结果表的一条数据;如果遍历完被驱动表没有找到满足连接条件的数据,则抛弃当前驱动表的数据,开始下一条数据。
  4. 循环遍历驱动表中的所有数据,得到最终联合查询结果集。

2.2 sql语句示例

使用inner join内连接关联用户和客户表信息

1
sql复制代码select u.name, u.gender, code from user u inner join customer c on u.id = c.userId;
  1. LEFT JOIN

left join,即左连接,是以左表为主进行的联合查询,最终结果是根据关联条件取两表交集,再加上左表独有的数据,若右表无对应数据,则相关字段设置为null值。

3.1 左连接查询流程

  1. 以左表为驱动表,右表为被驱动表
  2. 从左表中按照顺序取出数据,针对当前数据遍历右表判断是否满足连接条件
  3. 如果找到满足条件的数据,则将该数据放入结果表中;如果没有满足条件的数据,则返回左表的当前数据,并把右表该行对应的字段赋值为null。
  4. 选择下一条左表中的数据,继续进行步骤3,直到左表遍历完成后返回结果表。

3.2 sql语句示例

使用左外连接进行联合查询时,最终数据以左表为主,在此基础上进行右表的关联查询。

1
sql复制代码select u.name, u.gender, code from user u inner left join customer c on u.id = c.userId;
  1. RIGHT JOIN

right join,右表连接,是以右表为主进行的联合查询,右表作为驱动表,左表作为被驱动表。

如果将右表连接中的表顺序交换,并将right join换为left join,则是相同的执行结果。实际上,mysql在执行右表连接查询时,就是将其转换成左表连接查询进行的。

1
sql复制代码select u.name, u.gender, code from customer c inner right join user u on u.id = c.userId;
  1. FULL JOIN

full join,全表连接,是将驱动表和被驱动表中的数据取并集返回,没有对应内容时赋值为null。

mysql中没有专门的全表连接关键字,可以使用先left join取左连接,得到结果表r1,再right join 取右连接,得到结果表2,最后使用union将两个结果表合并,自动去除重复数据,得到并集数据。

  1. on和where的查询条件

  • 表连接时的连接条件使用on关键字来表示,on之后可以跟等值等判断条件,满足on后条件的数据会进行对应的关联连接。
  • 如果表连接的同时使用了where条件过滤数据,这相当于先将对应表中的数据根据where条件筛选后再进行两表的关联查询。
  • 可以使用where关键字可以on关键字定义条件
  • 如果表连接不使用关联条件,则结果是两个表的笛卡尔积

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

讲讲Python中的函数传递问题与其他补充 函数参数传递 参

发表于 2021-10-31

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

正式的Python专栏第25篇,同学站住,别错过这个从0开始的文章!

前篇讲了python中的函数和高阶函数,这篇再把函数的其他部分补充一下,稍微轻松简单一些。

函数参数传递

key-value方式传递参数:

1
2
3
python复制代码def show_info(name, title):
print("姓名为:",name)
print("职称为:",title)

我们在使用的时候可以用以下的方式:

1
2
arduino复制代码show_info('雷学委', '搬砖大师')
show_info(name='雷学委', title='搬砖大师')

动态长度参数传递

1
2
3
4
5
6
7
8
scss复制代码
def show_info_v2(name, title, *info):
print("姓名为:", name)
print("职称为:", title)
print("其他评价:", info)


show_info_v2('雷学委', '搬砖大师', "热爱技术", "热爱生活")

参数是否会被函数攥改?

我们看看下面的程序即可:

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
python复制代码#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/10/24 11:39 下午
# @Author : LeiXueWei
# @CSDN/Juejin/Wechat: 雷学委
# @XueWeiTag: CodingDemo
# @File : func_call.py
# @Project : hello


def compute_v1(list):
sum = 0
for x in list:
sum += x
list = list + [sum]
print("新地址:", id(list))
return sum


def compute_v2(list):
sum = 0
for x in list:
sum += x
list[0] = list[0] * 100
return sum


_list = [1, 2, 3, 4, 5]
print("调用计算函数v1之前:", _list)
print("调用计算函数v1之前内存地址:", id(_list))
print(compute_v1(_list))
print("调用计算函数v1之后:", _list)
print("调用计算函数v1之后内存地址:", id(_list))

_list = [1, 2, 3, 4, 5]
print("调用计算函数v2之前:", _list)
print("调用计算函数v2之前内存地址:", id(_list))
print(compute_v2(_list))
print("调用计算函数v2之后:", _list)
print("调用计算函数v2之后内存地址:", id(_list))

这里两个compute函数,一个修改参数引用地址,一个并没有修改参数引用而是修改了引用内存空间的关联地址(一个变量)的值。

可以说它们都成功了。但是外面的_list 的地址是任何时候都不会被修改的,除非赋值新地址(也就是在调用v2之前重新复制那次)

下面是运行结果:

屏幕快照 2021-10-31 下午10.34.46.png

其他 - Immutable 参数

如果传入不可串改的数据类型,那么尽管是基于引用关联地址来修改值(禁止写),比如传入一个由基础数据类型元素构成元组tuple作为参数,在调用函数中修改是被禁止。 Python运行的时候会提示错误!

对了,喜欢Python的朋友,请关注学委的 Python基础专栏 or Python入门到精通大专栏

持续学习持续开发,我是雷学委!

编程很有趣,关键是把技术搞透彻讲明白。

欢迎关注微信,点赞支持收藏!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…446447448…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%