2024年2月

由于Memory存储的是单纯的二进制字节,所以原则上我们可以用来它作为媒介,在wasm模块和数组程序之间传递任何类型的数据。在JavaScript API中,Memory通过WebAssembly.Memory类型表示,我们一般将它内部的缓冲区映射相应类型的数组进行处理。WebAssembly也提供了相应的指令来提供针对Memory的读、写、扩容等操作(源代码从
这里
下载)。

一、容量限制与扩容
二、内容的读写
三、内容初始化
四、多Memory支持
五、批量内存处理

一、容量限制与扩容

Memory本质上一个可以扩容的内存缓冲区,在初始化的时候我们必需指定该缓冲器的初始大小,单位为Page(64K)。如果没有指定最大允许的大小,意味着它可以无限“扩容”。WebAssembly.Memory的实例方法grow用来扩容,作为参数的整数表示扩大的Page数量,其返回值表示扩容之前的容量。在如下这个演示实例中,我们在一个Web页面index.html初始化的时候创建了一个WebAssembly.Memory对象,并将其初始和最大尺寸设置为1和3。

<html>
    <head></head>
    <body>
        <script>
            var memory= new WebAssembly.Memory({ initial: 1,  maximum: 3});
            var grow = (size) => {
                try{
                    console.log(`memory.grow(${size}) = ${memory.grow(size)}`);
                }
                catch(error){
                    console.log(error);
                }
            };
            grow(1);
            grow(1);
            grow(1);
        </script>
    </body>
</html>

grow函数对这个WebAssembly.Memory试试扩容。我们先后3次调用次函数(增扩的容量为1),并将其返回值打印到控制台上。从如下的输出可以看出,创建的Memory的初始容量为1,经过两次扩容后,它的容量达到运行的最大容量3,进而导致第三次扩容失败。

image

针对Memory的扩容也利用利用wasm的memory.grow指令来完成,该指令的输入参数依然是扩大的容量,返回的依然是扩容前的大小。如果超过设定的最大容量,该指令会返回-1。wasm还提供了memory.size指令返回Memory当前的容量。在如下这个wat文件(app.wat)中,我们依然定义了一个初始和最大容量为1和3的Memory,两个导出的函数size和grow分别返回它当前容量和对它实施扩容。

(module
   (memory 1 3)
   (func (export "size") (result i32)
      (memory.size)
   )
   (func (export "grow") (param $size i32) (result i32)
      (memory.grow (local.get $size))
   )
)

在作为宿主的index.html页面中,我们调用导出的grow函数(增扩的容量为1)对Memory实施3次扩容,并调用size函数输出它当前的容量。

<html>
    <head></head>
    <body>
        <script>
            var memory= new WebAssembly.Memory({ initial: 1,  maximum: 3});
            WebAssembly
                .instantiateStreaming(fetch("app.wasm"))
                .then((results) => {
                    var exports = results.instance.exports;
                    var grow = (size)=> console.log(`memory.grow(${size}) = ${exports.grow(size)}`);
                    grow(1);
                    grow(1);
                    grow(1);
                    console.log(`memory.size() = ${exports.size()}`);
        });
        </script>
    </body>
</html>

从如下的输出可以看出,前两次成功扩容将Memory的容量增扩到最大容量3,导致最后一次扩容失败,返回-1。

image

二、内容的读写

我们利用Memory对其管理的缓冲区按照纯字节的形式进行读写。WebAssembly针对具体的数据类型(i32/i64/f32/f64)提供一系列的load和store指令读写Memory的内容,具体的指令如下(8/16/32代表读写位数,s和u分别表示有符号和无符号整数):

  • {i32|i64|f32|f64}.load
  • {i32|i64}.load8_s
  • {i32|i64}.load8_u
  • {i32|i64}.load16_s
  • {i32|i64}.load16_u
  • {i32|i64}.load32_s
  • {i32|i64}.load32_u
  • {i32|i64|f32|f64}.store
  • {i32|i64}}.store8
  • {i32|i64}.store16
  • i64.store32

如下所示的WAT程序(app.wat)文件利用两个导出的函数store和load对导入的Memory实施写入和读取。我们假设存储的数据类型均为i32,所以store函数在执行i32.store指令的时候,代表写入序号的第一个参数需要乘以4,作为指令的第一个参数(代表写入的起始位置)。load函数在执行i32.load指令的时候也需要做类似的处理。

(module
   (memory (import "imports" "memory") 1)
   (func (export "store") (param $index i32) (param $value i32)
      (i32.store (i32.mul (local.get $index) (i32.const 4)) (local.get $value))
   )
   (func (export "load") (param $index i32) (result i32)
      (i32.load (i32.mul (local.get $index) (i32.const 4)))
   )
)

作为数组应用的JavaScript程序可以将Memory对象的缓冲区映射为指定元素类型的数组,并以数组的形式对其进行读写。在我们的演示实例中,作为宿主应用的index.html页面调用构造函数创建了一个WebAssembly.Memory对象,并将其buffer属性对应的缓冲区映射成一 个Int32Array对象,并将前三个元素赋值为1、2和3。我们将Memory对象导入到加载的app.wasm模块中后,调用导出的load函数以i32类型将Memory中存储的12个字节读出来。

<html> <head></head> <body> <script> var memory= new WebAssembly.Memory({ initial: 1, maximum: 3}); var array = new Int32Array(memory.buffer);
array[0] = 1; array[1] = 2; array[2] = 3; WebAssembly .instantiateStreaming(fetch("app.wasm"), {"imports":{"memory":memory}}) .then((results) => { var exports = results.instance.exports; console.log(`load (0) = ${exports.load(0)}`); console.log(`load (1) = ${exports.load(1)}`); console.log(`load (2) = ${exports.load(2)}`); }); </script> </body> </html>

从如下所示的三个输出结果可以看出,wasm模块中读取的内容与宿主应用设置的内容是一致的。

image

上面演示了wasm模块读取宿主应用写入Memory的内容,我们接下来通过修改index.html的内容调用导出的store函数往Memory中写入相同的内容,然后在宿主JavaScript程序中利用映射的数组将其读出来。

<html>
    <head></head>
    <body>
        <script>
            var memory= new WebAssembly.Memory({ initial: 1,  maximum: 3});
            var array = new Int32Array(memory.buffer);
            WebAssembly
                .instantiateStreaming(fetch("app.wasm"), {"imports":{"memory":memory}})
                .then((results) => {
                    var exports = results.instance.exports;
                    exports.store(0, 1);
                    exports.store(1, 2);
                    exports.store(2, 3);

                    console.log(`array[0] = ${array[0]}`);
                    console.log(`array[1] = ${array[0]}`);
                    console.log(`array[2] = ${array[0]}`);
        });
        </script>
    </body>
</html>

宿主程序从Memory中读取的内容体现在如下的输出结果中。

image

三、内容初始化

store指令一次只能往Memory对象的缓存区写入指定数据对象承载的全部或者部分字节,如果需要在初始化一长串字节(比如一大段文本),可以将其存储到data section中,data section会与Memory对象自动关联。在如下所示的WAT程序中(app.wat),我们声明了一个data section,并用它来存储一段文本(Hello World!),文本经过UTF-8编码后的字节将存储在此区域中。data指令的第一个参数 (i32.const 0)表示存储的起始位置。

(module
   (data (i32.const 0) "Hello, World!")
   (memory (export "memory") 1)
)

上面的WAT程序还定义并导出了一个Memory对象,利用它与data section的自动映射机制,我们可以利用Memory来读取存储的文本。在如下所示作为宿主应用的index.html中,我们提取出导出的Memory对象,并将其缓冲区映射为一个Int8Array对象,然后利用TextDescorder将其解码成文本并输出。

<html>
    <head></head>
    <body>
        <script>
            WebAssembly
                .instantiateStreaming(fetch("app.wasm"))
                .then((results) => {
                    var exports = results.instance.exports;
                    var array = new Int8Array(exports.memory.buffer, 0, 13);
console.log(new TextDecoder().decode(array)) }); </script> </body> </html>

从如下所示的输出结果可以看出,我们利用Memory成功读取了存储在data section的文本。

image

四、多Memory支持

WebAssembly目前的正式版本只支持“单Memory模式”,也就是说一个wasm只维护一个单一的Memory对象。虽然“多Memory”目前还处于实验阶段,但是目前主流的浏览器还是支持的,WAT程序中针对多Memory的程序又如何编写呢?在如下这个演示程序中,我们定义了4个Memory,并分别将其命名为$m0、$m1、$m2和$m3,其中前两个为导入对象,后两个为导出对象。我们将这4个Memory对象的初始化容量分别设置为1、2、3、4,导出的size函数用来返回指定Memory对象当前的容量。

(module
  (memory $m0 (import "imports" "memory1") 1)
  (memory $m1 (import "imports" "memory2") 2)
  (memory $m2 (export "memory3") 3)
  (memory $m3 (export "memory4") 4)

  (func (export "size") (param $memory i32) (result i32)
      (local $size i32)
      (local.set $size (memory.size $m0))

      (i32.eq (local.get $memory) (i32.const 1))
      if
         (local.set $size (memory.size $m1))
      end

      (i32.eq (local.get $memory) (i32.const 2))
      if
         (local.set $size (memory.size $m2))
      end

      (i32.eq (local.get $memory) (i32.const 3))
      if
         (local.set $size (memory.size $m3))
      end

      (local.get $size)
  )
)

size函数利用第一个参数(0、1、2、3)来确定具体的Memory对象,在执行memory.size的时候, 我们会附加上Memory的命名(默认为第一个Memory)。除了指定给定的别名,也可以按照如下的方式使用Memory的序号(0、1、2和3),其他指令的使用与之类似。

(module
  (memory (import "imports" "memory1") 1)
  (memory (import "imports" "memory2") 2)
  (memory (export "memory3") 3)
  (memory (export "memory4") 4)

  (func (export "size") (param $memory i32) (result i32)
      (local $size i32)
      (local.set $size (memory.size 0))

      (i32.eq (local.get $memory) (i32.const 1))
      if
         (local.set $size (memory.size 1))
      end

      (i32.eq (local.get $memory) (i32.const 2))
      if
         (local.set $size (memory.size 2))
      end

      (i32.eq (local.get $memory) (i32.const 3))
      if
         (local.set $size (memory.size 3))
      end

      (local.get $size)
  )
)

在执行wat2wasm对app.wat进行编译的时候,我们需要手工添加命令行开关--enable-multi-memory以提供针对“多Memory”的支持(wat2wasm app.wat -o app.wasm --enable-multi-memory)。

<html>
    <head></head>
    <body>
        <div id="container"></div>
        <script>
           var memory1 = new WebAssembly.Memory({initial:1});
           var memory2 = new WebAssembly.Memory({initial:2});
            WebAssembly
                .instantiateStreaming(fetch("app.wasm"), {"imports":{"memory1":memory1, "memory2":memory2}})
                .then((results) => {
                    var exports = results.instance.exports;

                    console.log(`memory1.size = ${exports.size(1)}`);
                    console.log(`memory2.size = ${exports.size(2)}`);
                    console.log(`memory3.size = ${exports.size(3)}`);
                    console.log(`memory4.size = ${exports.size(4)}`);
        });
        </script>
    </body>
</html>

在如上所示的作为宿主的index.html中,我们利用调用导出的size函数将四个Memory的初始容量输出到控制台上,具体的输出结果如下所示。

image

利用data section对Memory的填充同样也支持多Memory模式。如下面的代码片段所示,我们在app.wat中定义并导出了三个Memory,随后定义的三个data section通过后面指定的序号(默认为0)。我们将三个data section填充为对应的文本“foo”、“bar”和“baz”。

(module
  (memory (export  "memory1") 1)
  (memory (export  "memory2") 1)
  (memory (export  "memory3") 1)
  (data (i32.const 0) "foo")
  (data 1 (i32.const 0) "bar")
  (data 2 (i32.const 0) "baz")
)

作为宿主的index.html在获得导出的Memory对象后,同样将它们的缓冲区映射为Int8Array对象,并将其解码成字符串并输出到控制台上。

<html>
    <head></head>
    <body>
        <div id="container"></div>
        <script>
            WebAssembly
                .instantiateStreaming(fetch("app.wasm"))
                .then((results) => {
                    var exports = results.instance.exports;
                    var decoder = new TextDecoder();

                    var array = new Int8Array(exports.memory1.buffer, 0, 3);
                    console.log(`memory1: ${decoder.decode(array)}`);

                    array = new Int8Array(exports.memory2.buffer, 0, 3);
                    console.log(`memory2: ${decoder.decode(array)}`);

                    array = new Int8Array(exports.memory3.buffer, 0, 3);
                    console.log(`memory3: ${decoder.decode(array)}`);
        });
        </script>
    </body>
</html>

从三个导出的Memory中得到的字符串按照如下的形式输出到控制台上,可以看出它们与三个data section存储的内容是一致的。

image

五、批量缓冲处理

针对Memory的操作本质上就是针对字节缓冲区的操作,但是就目前发布的正式版本来说,相关的缓冲区操作还有待完善,不过很多都在“提案”里面了,其中就包括针对 bulk memory operations 。其中涉及如下一些有用的指令,它们已经在Web Assembly 最新的spec草案 里了,而且主流的浏览器也提供了部分支持。

  • memory.init: 从指定的data section中指定一段内存片段来初始化Memory;
  • memory.fill: 利用指定的字节内容来填充Memory的一段连续的缓冲区;
  • memory.copy:连续内存片段的拷贝;

接下来我们来演示一下针对memory.fill指令的应用。在如下所示的WAT程序中(app.wat),我们定义并导出了一个Memory对象。导出的fill函数调用memory.fill指令往导出的这个Memory指定的位置填充指定数量($count)的值($value)。

(module
  (memory (export "memory") 1)
  (func (export "fill") (param $offset i32) (param $value i32)  (param $count i32)
    (memory.fill (local.get $offset) (local.get $value) (local.get $count))
  )
)

在作为宿主的index.html页面中,我们两次调用导出的fill函数从Memory缓冲区的初始位置开始填充两个值255和266。

<html>
    <head></head>
    <body>
        <div id="container"></div>
        <script>
            WebAssembly
                .instantiateStreaming(fetch("app.wasm"))
                .then((results) => {
                    var exports = results.instance.exports;

                    exports.fill(0,255,2);
                    var array = new Int8Array(exports.memory.buffer, 0, 8);
                    array.forEach((value, index, _)=> console.log(`[${index}] = ${value}`));

                    exports.fill(0,256,2);
                    var array = new Int8Array(exports.memory.buffer, 0, 8);
                    array.forEach((value, index, _)=> console.log(`[${index}] = ${value}`));
        });
        </script>
    </body>
</html>

我们将缓冲区映射为一个Int8Array对象,并将其前8个字节输出到控制台上。作为memory.fill指令的第二个参数,表示填充值得数据类型应该是Byte,但是wasm支持的整数类型只有i32和i64,所以这里的参数类型只能表示为i32,但是该指令只会使用指定值的低8位。这一点可以从输出结果得到印证:第一次调用指定的值是255(00 00 00 FF,转换成Int8就是-1),最终只会填充前面2个字节(FF FF)。第二次调用指定的值为256(00 00 01 00),所以填充的前两个字节为00 00。

image

前言

随着
vue3.4
版本的发布,
defineModel
也正式转正了。它可以简化父子组件之间的双向绑定,是目前官方推荐的双向绑定实现方式。

vue3.4
以前如何实现双向绑定

大家应该都知道
v-model
只是一个语法糖,实际就是给组件定义了
modelValue
属性和监听
update:modelValue
事件,所以我们以前要实现数据双向绑定需要给子组件定义一个
modelValue
属性,并且在子组件内要更新
modelValue
值时需要
emit
出去一个
update:modelValue
事件,将新的值作为第二个字段传出去。

我们来看一个简单的例子,父组件的代码如下:

<template>
  <CommonInput v-model="inputValue" />
</template>

<script setup lang="ts">
import { ref } from "vue";

const inputValue = ref();
</script>

子组件的代码如下:

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

<script setup lang="ts">
const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
</script>

上面的例子大家应该很熟悉,以前都是这样去实现
v-model
双向绑定的。但是存在一个问题就是
input
输入框其实支持直接使用
v-model
的,我们这里却没有使用
v-model
而是在
input
输入框上面添加
value
属性和
input
事件。

原因是因为从
vue2
开始就已经是单向数据流,在子组件中是不能直接修改
props
中的值。而是应该由子组件中抛出一个事件,由父组件去监听这个事件,然后去修改父组件中传递给
props
的变量。如果这里我们给
input
输入框直接加一个
v-model="props.modelValue"
,那么其实是在子组件内直接修改
props
中的
modelValue
。由于单向数据流的原因,
vue
是不支持直接修改
props
的,所以我们才需要将代码写成上面的样子。

使用
defineModel
实现数据双向绑定

defineModel
是一个宏,所以不需要从vue中
import
导入,直接使用就可以了。这个宏可以用来声明一个双向绑定 prop,通过父组件的
v-model
来使用。

基础demo

父组件的代码和前面是一样的,如下:

<template>
  <CommonInput v-model="inputValue" />
</template>

<script setup lang="ts">
import { ref } from "vue";

const inputValue = ref();
</script>

子组件的代码如下:

<template>
  <input v-model="model" />
</template>

<script setup lang="ts">
const model = defineModel();
model.value = "xxx";
</script>

在上面的例子中我们直接将
defineModel
的返回值使用
v-model
绑定到input输入框上面,无需定义
modelValue
属性和监听
update:modelValue
事件,代码更加简洁。
defineModel
的返回值是一个
ref
,我们可以在子组件中修改
model
变量的值,并且父组件中的
inputValue
变量的值也会同步更新,这样就可以实现双向绑定。

那么问题来了,从
vue2
开始就变成了单向数据流。这里修改子组件的值后,父组件的变量值也被修改了,那这不就变回了
vue1
的双向数据流了吗?其实并不是这样的,这里还是单向数据流,我们接下来会简单讲一下
defineModel
的实现原理。

实现原理

defineModel
其实就是在子组件内定义了一个叫
model
的ref变量和
modelValue
的props,并且
watch
了props中的
modelValue
。当
props
中的
modelValue
的值改变后会同步更新
model
变量的值。并且当在子组件内改变
model
变量的值后会抛出
update:modelValue
事件,父组件收到这个事件后就会更新父组件中对应的变量值。

实现原理代码如下:

<template>
  <input v-model="model" />
</template>

<script setup lang="ts">
import { ref, watch } from "vue";

const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
const model = ref();

watch(
  () => props.modelValue,
  () => {
    model.value = props.modelValue;
  }
);
watch(model, () => {
  emit("update:modelValue", model.value);
});
</script>

看了上面的代码后你应该了解到了为什么可以在子组件内直接修改
defineModel
的返回值后父组件对应的变量也会同步更新了吧。我们修改的其实是
defineModel
返回的
ref
变量,而不是直接修改props中的
modelValue
。实现方式还是和
vue3.4
以前实现双向绑定一样的,只是
defineModel
这个宏帮我们将以前的那些繁琐的代码给封装到内部实现了。

其实
defineModel
的源码中是使用
customRef

watchSyncEffect
去实现的,我这里是为了让大家能够更容易的明白
defineModel
的实现原理才举的
ref

watch
的例子。如果大家对
defineModel
的源码感兴趣,请在评论区留言,如果感兴趣的小伙伴比较多,我会在下一期出一篇
defineModel
源码的文章。

defineModel
如何定义
type

default

既然
defineModel
是声明了一个prop,那同样也可以定义prop的
type

default
。具体代码如下:

const model = defineModel({ type: String, default: "20" });

除了支持
type

default
,也支持
required

validator
,用法和定义
prop
时一样。

defineModel
如何实现多个
v-model
绑定

同样也支持在父组件上面实现多个
v-model
绑定,这时我们给
defineModel
传的第一个参数就不是对象了,而是一个字符串。

const model1 = defineModel("count1");
const model2 = defineModel("count2");

在父组件中使用
v-model
时代码如下:

<CommonInput v-model:count1="inputValue1" />
<CommonInput v-model:count2="inputValue2" />

我们也可以在多个
v-model
中定义
type

default

const model1 = defineModel("count1", {
  type: String,
  default: "aaa",
});

defineModel
如何使用内置修饰符和自定义修饰符

如果要使用系统内置的修饰符比如
trim
,父组件的写法还是和之前是一样的:

<CommonInput v-model.trim="inputValue" />

子组件也无需做任何修改,和上面其他的
defineModel
例子是一样的:

const model = defineModel();

defineModel
也支持自定义修饰符,比如我们要实现一个将输入框的字母全部变成大写的
uppercase
自定义修饰符,同时也需要使用内置的
trim
修饰符。

我们的父组件代码如下:

<CommonInput v-model.trim.uppercase="inputValue" />

我们的子组件需要写成下面这样的:

<template>
  <input v-model="modelValue" />
</template>

<script setup lang="ts">
const [modelValue, modelModifiers] = defineModel({
  // get我们这里不需要
  set(value) {
    if (modelModifiers.uppercase) {
      return value?.toUpperCase();
    }
  },
});
</script>

这时我们给
defineModel
传进去的第一个参数就是包含
get

set
方法的对象,当对
modelValue
变量进行读操作时会走到
get
方法里面去,当对
modelValue
变量进行写操作时会走到
set
方法里面去。如果只需要对写操作进行拦截,那么可以不用写
get

defineModel
的返回值也可以解构成两个变量,第一个变量就是我们前面几个例子的
ref
对象,用于给
v-model
绑定。第二个变量是一个对象,里面包含了有哪些修饰符,在这里我们有
trim

uppercase
两个修饰符,所以
modelModifiers
的值为:

{
  trim: true,
  uppercase: true
}

在输入框进行输入时,就会走到
set
方法里面,然后调用
value?.toUpperCase()
就可以实现将输入的字母变成大写字母。

总结

这篇文章介绍了如何使用
defineModel
宏实现双向绑定以及
defineModel
的实现原理。

  • 在子组件内调用
    defineModel
    宏会返回一个
    ref
    对象,在子组件内可以直接对这个
    ref
    对象进行赋值,父组件内的相应变量也会同步修改。
  • defineModel
    其实就是在子组件内定义了一个ref变量和对应的prop,然后监听了对应的prop保持ref变量的值始终和对应的prop是一样的。在子组件内当修改ref变量值时会抛出一个事件给父组件,让父组件更新对应的变量值,从而实现双向绑定。
  • 使用
    defineModel({ type: String, default: "20" })
    就可以定义prop的
    type

    default
    等选项。
  • 使用
    defineModel("count")
    就可以实现多个
    v-model
    绑定。
  • 通过解构
    defineModel()
    的返回值拿到
    modelModifiers
    修饰符对象,配合
    get

    set
    转换器选项实现自定义修饰符。

如果我的文章对你有点帮助,欢迎关注公众号:【欧阳码农】,文章在公众号首发。你的支持就是我创作的最大动力,感谢感谢!

本文介绍基于
C++
语言
GDAL
库,
批量创建
大量栅格遥感影像文件,并将数据批量写入其中的方法。

首先,我们来明确一下本文所需实现的需求。已知我们对
大量遥感影像
进行了批量读取与数据处理操作——具体过程可以参考文章
C++ GDAL提取多时相遥感影像中像素随时间变化的数值数组
;而随后,就需要对我们处理后的栅格数据再进行输出,即建立新的大量的栅格遥感影像,并将我们处理后的像元数据依次输入进去。

明确了具体需求,接下来就可以开始代码的实践;本文所用到的具体代码如下。这里需要注意,在这里就仅将与本文需求有关的代码放了上来,其他无关的代码就省略了(所以以下代码只是程序主函数中的一部分);大家在实践过程中,依据自己的需求,将自己代码与本文的代码相结合就可以。

#include <iostream>
#include "gdal_priv.h"

//以下只列出栅格数据批量创建、写入与导出的代码,其他无关的代码就省略了~

	int pic_index_2 = 1;
	for (auto x : my_file)
	{
		const char* pszFormat = "GTiff";
		GDALDriver* poDriver;
		GDALAllRegister();
		poDriver = GetGDALDriverManager()->GetDriverByName(pszFormat);
		if (poDriver == NULL)
			cout << "There is an error with poDriver!" << endl;
		GDALDataset* poSrcDS = (GDALDataset*)GDALOpen(mod_file.c_str(), GA_ReadOnly);
		GDALDataset* poDstDS;
		char** papszOptions = NULL;
		papszOptions = CSLSetNameValue(papszOptions, "TILED", "YES"); //建立金字塔
		papszOptions = CSLSetNameValue(papszOptions, "COMPRESS", "LZW");

		int pos_need = x.rfind("CSI");
		string file_name = x.substr(pos_need);
		string out_file = out_path + "//R_" + file_name;
		poDstDS = poDriver->CreateCopy(out_file.c_str(), poSrcDS, FALSE, papszOptions, GDALTermProgress, NULL);

		GDALRasterBand* poOutBand;
		poOutBand = poDstDS->GetRasterBand(1);
		poOutBand->RasterIO(GF_Write, 0, 0, nXSize, nYSize, out_pafScanline[pic_index_2 - 1], nXSize, nYSize, GDT_Float64, 0, 0);

		pic_index_2++;

		GDALClose((GDALDatasetH)poDstDS);
		GDALClose((GDALDatasetH)poSrcDS);
	}

	delete[] pafScanline;
	delete[] out_pafScanline;
	delete[] pixel_paf;
	delete[] pixel_paf_result;
	pafScanline = NULL;
	out_pafScanline = NULL;
	pixel_paf = NULL;
	pixel_paf_result = NULL;

以上代码的思路其实也非常简单。首先,因为是需要对大量的栅格进行批量操作,所以代码整体是在
for
循环中进行的,每一个循环都是对一个独立的栅格文件的创建、数据写入与文件保存操作;其中,
"GTiff"
表示我们将要生成的栅格文件是
.tif
格式的,如果大家需要生成别的格式的话可以修改这里;
auto x : my_file
表示从我们前期已经获取到的
需要处理的栅格文件列表
中遍历(虽然我们这里是需要建立新的栅格文件,但由于我这里新的栅格文件的命名规则是与原有的栅格文件一致的,所以就还是从原有的文件列表中遍历),
my_file
就是前期已经获取到的需要处理的栅格文件列表,具体获取方法可以参考文章
C++遴选出特定类型的文件或文件名符合要求的文件

接下来,就是基于
GDAL
库来实现栅格数据的创建与写入。在
GDAL
库中,如果我们想用自己的数据生成栅格文件,首先需要基于
CreateCopy()
函数新建一个栅格文件,随后通过
RasterIO()
函数写入数据。其中,
poSrcDS
是一个指向模板栅格文件的指针;在我们用
CreateCopy()
函数新建栅格文件时,新的栅格文件的各项属性,比如行数、列数、像元大小、坐标信息等,都直接与这个模板栅格文件保持一致。随后,
poDstDS
则是指向我们此时将要新建的栅格文件的指针。

接下来,我们通过
CSLSetNameValue()
函数,配置一下将要生成的新的栅格文件的属性,比如
"TILED", "YES"
表示栅格文件同时生成金字塔,
"COMPRESS", "LZW"
表示栅格文件通过
LZW
算法进行压缩等。

再接下来,是配置我们新的栅格文件的文件名的代码部分。因为我们是需要批量生成大量的栅格文件的,所以其文件名肯定不能手动逐一修改;我这里就是直接在已有文件的文件名基础上,增添了一个字母,作为新栅格文件的文件名;这里就是通过字符串的截取等操作来实现新的文件名的生成。其中,
out_path
是我们已经定义过的变量,表示结果保存路径。

完成以上全部配置后,即可依据
CreateCopy()
函数进行新的栅格文件的创建。

至此,我们仅仅是完成了
GDAL
库中栅格文件的创建,但此时还没有将数据导入进去,因此在资源管理器中也是看不到具体的新的栅格文件的。随后,我们基于
RasterIO()
函数,将数据写入栅格文件即可;其中,
out_pafScanline[pic_index_2 - 1]
就是需要写入到每一景遥感影像中的数据。

完成以上工作后,我们就完成了对其中一景遥感影像的创建、写入,此时资源管理器中就会看到这一景图像的文件已经存在。随后,通过
GDALClose()
函数将刚刚指向的栅格遥感文件关闭,并进行下一次循环。对全部需要生成的栅格遥感影像文件都完成遍历后,则通过
delete[]

= NULL
等语句释放内存、取消指针。

此时,即可在目标文件夹中看到我们批量生成的栅格文件。

基于此,即可完成批量创建、写入栅格数据的操作。


引言


大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第七篇内容:重排序与Happens-Before。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!!

在上一篇文章中,我们简单提了一下重排序与Happens-Before。在这篇文章中我们将深入讲解一下重排序与Happens-Before,然后再结合Java内存模型一起学习。让大家对Java内存模型和重排序与Happens-Before有更加深入的了解。

在当今的计算机系统中,多线程编程已经成为提升应用程序性能和响应能力的关键技术。Java作为现代开发语言中的翘楚,提供了强大的多线程支持,并通过Java内存模型(JMM)来规范并确保多线程环境下数据的正确性和一致性。然而,在追求高性能的过程中,编译器优化以及处理器为了最大化利用指令级并行性而引入的重排序机制成为程序员理解并发行为时的一大挑战。

重排序,简单来说,是指编译器或处理器为了提高执行效率,在不改变单线程程序结果的前提下,对指令进行重新排列的一种策略。例如,当两个独立操作之间不存在数据依赖时,处理器可以灵活调整它们的执行顺序,使得流水线处理更高效,从而避免不必要的等待时间。但是,这种灵活性在多线程环境中可能带来不确定性,因为不同的线程可能观察到由于重排序而导致的不同内存状态,进而引发难以预测的行为和潜在的错误。

为了更好地管理这种复杂性,Java内存模型定义了happens-before原则,这一概念为开发者提供了一套简洁且强健的规则,以保证跨线程的内存可见性和执行顺序的一致性。比如,根据天然的happens-before关系,一个线程内按代码顺序执行的操作具有先行发生的关系;监视器锁的解锁操作必定先于后续对该锁的加锁操作;volatile变量的写入操作会先行于任何后续对同一volatile变量的读取操作。

下面举个简单的例子说明happens-before规则的应用:

int a = 1// 操作A
int b = 2// 操作B
int sum = a + b; // 操作C

System.out.println(sum);

在这个单一线程的示例中,虽然JVM理论上可能对操作A和操作B进行重排序(例如先执行B后执行A),但由于happens-before规则的存在,我们可以确定操作A的结果(a=1)对于操作C是可见的,即无论实际执行顺序如何,最终输出的
sum
值总是3。而在多线程场景下,happens-before原则更为重要,它能帮助我们构建出符合预期的同步逻辑,防止因重排序带来的数据竞争与不一致现象。

因此,深入理解和应用重排序与happens-before原则对于编写稳定、高效的Java多线程程序至关重要,它能够帮助开发者有效地规避并发陷阱,确保程序在高度并发环境下的正确运行。本文将详细介绍重排序的类型及其影响,同时结合Java内存模型阐述happens-before规则的具体内容和应用场景,以便读者能够在实践中合理运用这些理论知识。


重排序的概念与分类


在Java多线程编程中,重排序是一个至关重要的概念,它直接影响到并发程序的执行结果和内存可见性。重排序是指编译器或处理器为了优化性能,在不违反单线程语义的前提下,对指令执行顺序进行调整的过程。

定义重排序
在计算机系统内部,程序中的指令并非严格按照源代码的顺序执行。当一个CPU核心通过流水线技术处理指令时,若前一条指令未完成但不影响后续指令的执行,处理器可能会提前执行下一条或多条指令。同样地,编译器在生成机器代码的过程中也可能出于优化目的重新安排源代码的执行顺序。这种现象就被称为指令重排序。

例如,考虑以下简单的操作序列:

int a = 1;
int b = 2;
int sum = a + b;

理论上,编译器或处理器可以将加载
b
的值的操作提前到计算
a + b
之前,只要这样的重排不会改变单线程程序的预期输出结果。

重排序的类型
重排序主要分为以下三种:

  • 编译器优化重排 :编译器在翻译高级语言为低级语言时,会进行各种优化措施,如删除冗余代码、合并循环内的不变量计算等。其中一个优化就是指令调度,编译器根据数据依赖关系分析出哪些指令之间可以互换顺序而不影响最终的单线程执行结果。
  • 指令并行重排 :现代处理器普遍采用了指令级并行技术,允许若干条无数据依赖性的指令同时执行。比如,两个独立变量间的赋值操作可以交错进行,因为它们的结果彼此独立,不需要等待对方完成。在这种情况下,处理器层面的乱序执行机制实际上也是一种重排序。
  • 内存系统重排 :在多级缓存系统中,由于缓存一致性协议的存在,不同线程在读取或写入同一变量时可能观察到不同的执行顺序。例如,某个线程先进行了一个写操作,但由于缓存未及时刷新至主内存,其他线程可能无法立即看到这个写操作的结果,这就表现为一种内存系统的重排序。

综上所述,虽然重排序提高了CPU利用率和程序执行效率,但它也可能引入了潜在的多线程问题,尤其是在没有正确同步的情况下,可能导致不可预测的行为和数据竞争。为此,Java内存模型(JMM)通过happens-before规则来限制重排序,并确保在正确同步的多线程环境中,各线程能观察到一致且符合预期的内存状态。


顺序一致性模型与JMM保证


顺序一致性模型
顺序一致性内存模型是一种理想化的理论模型,它假设程序在单线程和多线程环境中的执行都像在一个全局的、严格的串行环境中进行。在这个模型中,有两个核心特性:

  1. 单线程内部操作顺序性 :一个线程内的所有操作必须按照它们在源代码中出现的顺序来执行。
  2. 全局操作视图的一致性 :不论程序是否同步,所有线程看到的操作顺序都是相同的,即每个操作对所有线程而言都是原子且立即可见的。

例如,在两个并发线程A和B中,如果线程A有三个操作A1、A2、A3,线程B有三个操作B1、B2、B3,并且线程A正确释放锁后线程B获取同一把锁,那么在顺序一致性模型下,两个线程会观察到一个整体有序的操作序列,如 A1->A2->A3->B1->B2->B3。

然而,实际硬件和编译器并不遵循如此严格的顺序一致性模型,而是允许一定的指令重排序以提升性能。

数据竞争与顺序一致性
当程序没有进行正确的同步控制时,就可能出现数据竞争问题。数据竞争指的是在一个线程内写入变量的同时,另一个线程读取了同一个变量,且这两个操作之间没有通过任何同步机制来确保执行顺序。这种情况下,程序的行为可能变得不可预测,例如读取到未更新的数据或者状态混乱。

// 示例:数据竞争
class DataRaceExample {
    int sharedValue = 0;

    Thread writerThread = new Thread(() -> {
        sharedValue = 1// 写操作
    });

    Thread readerThread = new Thread(() -> {
        int localCopy = sharedValue; // 读操作
        System.out.println("Reader sees: " + localCopy);
    });

    public void startThreads() {
        writerThread.start();
        readerThread.start(); // 数据竞争,因为没有同步措施
    }
}

在这个示例中,读者线程可能会在写者线程完成赋值之前就读取
sharedValue
,从而导致结果不确定。

JMM对未同步程序的限制
Java内存模型(JMM)并没有承诺为未正确同步的多线程程序提供与顺序一致性模型一致的执行效果。对于未同步的程序,JMM仅提供了最小安全性保证——即一个线程读取到的值要么是其他线程之前写入的值,要么是初始化的默认值。

为了实现这一最低安全边界,JVM在堆上分配对象时,会先清零整个内存区域再进行对象的构造,以确保即使在并发环境下也能避免无中生有的数据现象。

尽管JMM没有强制要求所有操作对所有线程立即可见,但它针对使用了关键字
synchronized

volatile
以及
final
等正确同步的代码部分做出了明确的内存一致性保证。在临界区(synchronized块或方法)内,虽然可以发生重排序,但JMM通过锁的内存语义确保了这些重排序不会被其他线程观测到。同时,在进入和退出临界区时,JMM通过特殊处理使得临界区内代码能够获得如同顺序一致性模型下的内存视图,进而确保正确同步程序的执行结果与顺序一致性模型中的执行结果相同。


happens-before原则详解


happens-before概念
在Java内存模型(JMM)中,
happens-before
关系是一种定义线程间操作执行顺序的准则。它确保了如果一个操作A
happens-before
操作B,那么操作A的结果对操作B是可见的,并且操作A的执行顺序发生在操作B之前。这一原则为程序员提供了一种简洁的方式来理解多线程环境中的内存可见性和执行顺序保证。

天然的happens-before关系
Java语言中存在着一系列天然的
happens-before
关系:

  1. 程序顺序规则
    :在一个线程内部,按照源代码顺序执行的每个操作都
    happens-before
    该线程内任意后续的操作。

    int x = 0// A操作
    x = 1;    // B操作

    在此例中,根据程序顺序规则,操作A(初始化x为0)
    happens-before
    操作B(将x赋值为1)。

  2. 监视器锁规则
    :对同一个锁对象解锁操作
    happens-before
    随后对该锁的加锁操作。

    synchronized (lock) {
        // 写操作...
        lock.unlock(); // unlock happens-before 下一次的 lock()
    }
    synchronized (lock) {
        lock.lock(); // 加锁操作,在unlock之后
        // 读操作...
    }

  3. volatile变量规则
    :对volatile变量的写操作
    happens-before
    于任意后续对同一volatile变量的读操作。

    volatile boolean ready = false;

    void writer() {
        ready = true// 写操作
    }

    void reader() {
        if (ready) { // 读操作
            // 执行相关逻辑
        }
    }

  4. 传递性
    :如果A
    happens-before
    B,且B
    happens-before
    C,那么可以推导出A
    happens-before
    C。

  5. start规则
    :线程A启动线程B,那么线程A中调用
    ThreadB.start()
    的操作
    happens-before
    线程B内的任何操作。

  6. join规则
    :线程A成功地调用线程B的
    join()
    方法并返回,意味着线程B中的所有操作
    happens-before
    线程A从
    join()
    方法返回后执行的任何操作。

重排序与happens-before的关系
JMM允许两种类型的重排序:不会改变程序执行结果的重排序和会改变结果的重排序。对于前者,编译器和处理器可以自由进行优化;而对于后者,JMM严格禁止。

例如,在单线程环境下,虽然可能存在指令重排序使得操作A和操作B的执行顺序与源码顺序不一致,但如果操作A和操作B之间存在
happens-before
关系,则无论实际执行时如何重排,操作A对操作B的可见性都将得到保障。

int a = 1// A操作
int b = 2// B操作
int sum = a + b; // C操作

System.out.println(sum);

在这个例子中,尽管编译器或处理器可能会对操作A和操作B进行重排序,但由于它们在同一线程内按源代码顺序执行,因此遵循程序顺序规则,即使发生重排序也不会影响最终结果。因此,
happens-before
原则确保了只要正确遵守这些规则,程序员就无需关心具体实现层面的指令重排序,而能够专注于程序本身的逻辑。

总的来说,
happens-before
原则为Java多线程编程提供了强有力的工具来理解和控制并发环境下的内存行为,通过合理利用这些规则,可以避免数据竞争和不确定性的出现,确保程序在多线程场景下表现出预期的一致性和正确性。


注意事项


总结上述讨论,重排序是多线程编程中不可忽视的重要概念。为了提高程序执行效率,编译器和处理器会进行指令重排,然而这种优化在并发环境下可能引入不确定性,导致数据竞争、内存一致性问题以及难以预测的程序行为。为了解决这些问题,Java内存模型(JMM)通过定义happens-before原则提供了对多线程程序执行顺序的明确约定。

在实践中,理解并正确应用happens-before规则至关重要:

  1. 确保同步逻辑
    :利用Java语言中的天然happens-before关系如监视器锁规则(synchronized关键字)、volatile变量规则等来创建明确的操作顺序约束。例如,在一个多线程共享资源的场景中,使用
    synchronized
    方法或块确保写操作完成后才能读取该资源,从而避免数据竞争。

    class SharedResource {
        private int sharedValue;

        public synchronized void write(int newValue) {
            this.sharedValue = newValue;
        }

        public synchronized int read() {
            return this.sharedValue;
        }
    }

  2. 识别潜在的数据竞争
    :如果程序中存在未同步访问共享变量的情况,应仔细审查代码以识别潜在的数据竞争,并通过适当的同步机制来消除。例如,若两个线程同时读写一个非volatile变量,则可能导致结果不确定,此时应当声明变量为volatile或使用锁来保证可见性和有序性。

  3. 遵循happens-before原则编写并发代码
    :在设计并发组件时,要牢记happens-before规则,确保操作之间的依赖关系得到妥善处理。例如,可以利用start/join规则来构建线程间的happens-before链,确保一个线程完成特定任务后,其他线程能够看到其更新的结果。

  4. 测试与调试
    :在开发过程中,通过单元测试和多线程环境下的集成测试验证程序在不同情况下的行为是否符合预期。对于复杂的并发问题,可利用Java提供的工具如
    Atomic
    类、
    ThreadLocal
    变量以及各种并发容器来简化并发控制,同时结合Java内存模型的理解来排查和修复因重排序引发的问题。

总之,深入理解和运用重排序及happens-before原则能有效指导开发者编写出更稳定、一致且高效的多线程Java应用程序。在实际项目中,务必重视并发控制和内存可见性问题,不断实践和优化同步策略,从而提升程序在并发环境下的表现。

本文使用
markdown.com.cn
排版

前言

笔者目前从事一线 Java 开发今年是第 3 个年头了,从 0-1的 SaaS、PaaS 的项目做过,多租户下定制化开发项目也做过,项目的 PM 也做过...

在实际的开发中积累了一些技巧和经验,包括线上 bug 处理、日常业务开发、团队开发规范等等。现在在这里分享出来,作为成长的记录和知识的更新,希望与大家共勉。

免责声明:以下所有demo、代码和测试都是出自笔者本人的构思和实践,不涉及企业隐私和商业机密,属于个人的知识分享。

一、枚举类的注解

看起来很常见的枚举,可能也隐藏着使用上的问题:你有没有在代码里不小心做过改变枚举值的操作?或者为怎么合理规范地写构造方法/成员方法而烦恼?

那么不妨来看看我的示例,注释写得比较清楚了:

@Getter// 只允许对属性 get,不允许 set
@RequiredArgsConstructor// 为枚举的每个属性生成有参构造
public enum ProjectStatusEnum {
    SURVEY("已调研"),
    APPROVAL("已立项"),
    PROGRESSING("进行中"),
    COMPLETED("已完成");
    // 对该成员变量使用 final 来修饰,表明一旦赋值就不可变
    private final String name;
}

为什么要分享这个呢?团队里开发的时候还真有人在使用枚举的时候 set() 改变了枚举值,编译通过但运行在一定条件触发后,导致了 bug 排查了一下午。


二、RESTful 接口

本节的内容其实更像是一种规范,因为见过不少别的部门同事写的项目代码,接口的风格真是迥异(写什么的都有),当我接手重构的时候真是头皮发麻。

首先就是禁止使用 Swagger 和 Knife4j 接口文档生成工具,原因无它:代码侵入性太强和需要写的注解太多,而且还是公司安全漏洞扫描单上的常客。

其次可以使用开源的 smart-doc 来代替,只要遵循 Javadoc 的标准注释写法即可。

  • 请求方式和参数

    在 Controller 中一般只使用 GET 请求和 POST 请求,无需使用 PUT 和 DELETE。

    GET 请求一般只使用 @GetMapping,主要配合 @RequestParam、@PathVariable 使用,请求的拼接参数一般不超过 5 个。

    POST 请求一般只使用 @PostMapping,主要配合 @RequestBody 使用,请求参数统一使用 DTO 对象。

    如果入参需要带上请求头,可以加上 @RequestHeader 注解;如果是提交表单,可以加上 @Valid 做参数校验,如 @NotBlank 和 @NotNull 等。

  • 统一返回

    主要包括:返回体(返回码+信息+返回数据)+ 封装VO + 统一异常,这些基础的东西肯定是遵顼团队/公司的开发规范,不需要再自己造轮子。

一个简单的 Controller 示例如下:

/**
 * 测试接口
 */
@RestController
@RequestMapping("/study")
public class StudyController {

    @Resource
    private StudyService studyService;

    /**
     * 新增xx
     * @return 是否成功
     */
    @PostMapping("/add")
    //还可以加上其它必要注解,如:登录/权限/日志记录等
    public Response<Boolean> addStudy(@RequestBody @Valid StudyDTO studyDTO) {
        return ResultUtils.success(studyService.addStudy(studyDTO));
    }

    /**
     * xx列表(不分页)
     * @return 列表数据
     */
    @GetMapping("/list")
    public Response<List<StudyListVO>> getList(@RequestParam("id") String id) {
        return ResultUtils.success(studyService.getList(id));
    }
}


三、类属性转换

在实际 Java 开发中,关于 VO、Entity、DTO 等对象属性之间的赋值是我们经常遇见的,
最简单使用 @Data 去逐个 .set() 或者 @Builder 链式 .build()
,其实都是很靠谱的办法,而且可以控制颗粒度。但属性一多起来的话,比如二十个以上,那么代码就会显得很长。所以有没有办法一行代码就搞定类属性转换呢?

首先不推荐使用 BeanUtils.copyProperties() 作类属性的拷贝
,以下是几个常见的坑:

  1. 同一字段分别使用包装类型和基本类型,会出现转换异常,不会灵活识别转换
  2. null 值覆盖导致数据异常,即源属性有值为 null,但是目标属性有正常值,拷贝后会被 null 覆盖
  3. 内部类属性无法正常拷贝,即使类型和字段名均相同也无法拷贝成功,这个真的很坑

推荐
泛型 + JSON
组合的方式来实现类属性的转换,具体步骤如下:

  • 定义一个父类 CommonBean,
    让项目里所有 VO、Entity、DTO 等类都继承该类
    ,类里面就只定义一个公共的泛型方法即可:

    public class CommonBean implements Serializable {
        /**
         * @apiNote 全局类型转换方法:入参和返参均支持泛型
         * @param target
         * @return 目标类型
         * @param <T>
         */
        public <T> T copyProperties(Class<T> target) {
            //本质上就是进行了 Object -> json字符串 -> 到指定类型的转换
            return JSON.parseObject(JSON.toJSONString(this), target);
        }
    }
    
  • 在需要转换的地方,直接调用上面定义的方法即可完成转换:

        @Test
        public void testCopyProperties(){
            //Worker 和 WorkerVO 都需要 extends 上述的 CommonBean
            Worker worker = new Worker();
            worker.setName("Alex");
            worker.setStatus(NumberUtils.INTEGER_ONE);
            //直接使用,得到需要的目标 VO 对象
            WorkerVO workerVO = worker.copyProperties(WorkerVO.class);
            log.info("转换结果:{}",workerVO);
        }
    


四、Stream 流

  • map() 流元素的映射、collect() 收集流,这两个就不展开讲了,几乎可以说是 Stream 流用的最多的方法,到处都是例子。

  • filter() 流按条件过滤和 sort() 流元素排序,这两个组合可以简单举个例子:

    /**
     * Stream 流的过滤与排序
     * @param id
     * @return 列表数据
     */
    @Override
    public List<StudyVO> getList(String id) {
       List<StudyVO> resultList = this.list(new LambdaQueryWrapper<Study>()
                .eq(Study::getIsDelete, NumberUtils.INTEGER_ZERO)).stream()
                .filter(e -> Constants.USER_ROLE_USER.equals(e.getUserRole()))
                .sorted(Comparator.comparing(Study::getAge).reversed())
                .map(e -> e.copyProperties(StudyVO.class)).collect(Collectors.toList());
        return Optional.of(resultList).orElse(null);
    }

像上述从MySQL 里查表数据的例子,
其实能在数据库做的操作就没必要在 Stream 流里操作。
像 .select()、.eq()、.gt()、.orderByDesc() 等都可以完成,非数据库语句查询的情况下,使用 Stream 操作集合还是有必要的。

  • anyMatch() 平时用的不多,但当遇到了之后,它的用法还是能让人眼前一亮的:
 /**
     * 测试 Stream 的 AnyMatch 方法
     * @return
     */
    public List<ArticleVO> testStreamAnyMatch(){
        List<Article> articleList = this.list(new LambdaQueryWrapper<Article>().eq(Article::getIsDelete, NumberUtils.INTEGER_ZERO));
        if (CollectionUtils.isNotEmpty(articleList)){
            //AnyMatch() 方法返回的是一个布尔,用来判断流中是否有满足条件的元素
            final boolean flag = articleList.parallelStream().anyMatch(e -> Objects.nonNull(e)
                    //文章要有内容
                    && Objects.nonNull(e.getContent())
                    //文章要有标题
                    && StringUtils.isNotBlank(e.getTitle()));
            if (flag){
                return articleList.parallelStream().map(e -> e.copyProperties(ArticleVO.class)).collect(Collectors.toList());
            }
        }
        return new ArrayList<>();
    }
  • skip() 跳过流的某些元素和 limit() 指定流元素的位置,
    组合起来使用可以实现自分页
    。这个在
    线上问题-如何避免集合内存溢出
    的文章中会拿出来单独讲一下。


五、判空和断言

5.1判空部分

首先什么情况下需要判空?基本是以下这 3 种情况:

  1. 当进行对象引用操作时

    为了避免 NPE 通常需要先判断该对象是否为 null。比如,在调用对象的方法或访问其属性之前,应该首先判断该对象不为 null 。

        @Test
        public void testNullMethod(){
            //以下对象为 null,即表示该对象的变量在内存中不引用任何对象地址
            Study study = null;
            //则下面调用该对象的 get() 方法试图获得其属性,则会导致 NPE
            String userName = study.getUserName();
            log.info("用户名称:{}", userName);
        }
    
  2. 从外部获取数据返回时

    比如:从数据库查询数据返回、调用另一个接口的方法返回的数据,有可能返回对象的结果为 null:

        @Override
        public StudyVO detail(String id) {
            //查 MySQL 返回的集合数据,即使 null 赋值也没有问题
            List<Study> resultList = this.list(new LambdaQueryWrapper<Study>()
                    .eq(Study::getIsDelete, NumberUtils.INTEGER_ZERO)
                    .orderByDesc(Study::getAge));
            //但是同样需要对集合对象进行判空,有值再进行下一步
            if (CollectionUtils.isNotEmpty(resultList))
            {
                return resultList.get(NumberUtils.INTEGER_ZERO).copyProperties(StudyVO.class);
            }
            return new StudyVO();
        }
    
  3. 当集合进行元素操作时

    由于集合本身并没有提供直接的判空机制,所以在进行元素操作之前,需要进行集合是否为空的判断:

        /**
         * 举个反例
         */
    	@Override
        public StudyVO detail(String id) {
            //调用其它方法,并将返回结果赋值,是 null 也没问题
            List<StudyVO> voList = this.getList(id);
            //但在使用 get() 方法前不判断集合里有没有元素,那么会报数组越界
            return voList.get(NumberUtils.INTEGER_ZERO);
        }
    

那么,常用判空的工具有哪些呢?从我个人的开发经验来说主要有以下几种:

  • 对象的判空

    推荐统一使用 java.util 包的 Objects.nonNull() 等方法。

  • 集合的判空

    推荐统一使用 org.apache.commons.collections.CollectionUtils 包的 .isNotEmpty() 等方法。

  • Map 对象判空

    推荐统一使用 Map 自带的 .isEmpty() 、 .containsKey()、.equals() 这三者配合使用。

  • 字符串的判空

    推荐统一使用 org.apache.commons.lang3.StringUtils 包的 .isNotBlank() 等方法。

  • Optional类

     Optional
         //of(T value)方法用于创建一个包含指定值的 Optional 对象,该方法接收一个非 null 值作为参数
       .of()
         //ofNullable(T value)方法用于创建一个包含指定值的 Optional 对象,该方法接收一个可能为 null 的值作为参数
         .ofNullable()
         //isPresent()方法用于判断 Optional 对象中是否存在非 null 值,有值就返回 true ,否则返回 false
         .isPreset()
         //orElse(T other)方法顾名思义,泛型 T 表示其它的类型
         .orElse()
         //ifPresent(Consumer<? super T> consumer) 判断该对象是否值,有则调用传入的 Consumer 类型函数处理该值。否则,什么也不做
         .ifPresent()  
         //map(Function<? super T, ? extends U> mapper) 用于对 Optional 对象中的值进行映射,并返回一个新的 Optional 对象
         .map()
         //filter(Predicate<? super T> predicate) 用于过滤 Optional 对象中的值,只有当值满足特定条件时才保留
         .filter()
    

什么情况下可以不需要判空?

答:一般当方法允许直接返回 null 时,可以不对返回值进行判空。

5.2断言部分

断言的应用场景可能就比较单一了,我自己的理解是
:在业务因为无数据且需要强制中断业务时,可以使用到断言。
那么接口允许返回空、或者返回约定的状态码等这些情况除外。

一定程度上可以简化“先判断,再抛异常”的代码。即替换以下代码,三行变一行:

    if (Objects.nonNull(Object obj)){
        throw new BusinessException("obj error");
    }
//对象型断言:如果该对象为 null ,则抛出 String 类型的异常信息
Assert.notNull(@Nullable Object object, String message);

常用的断言如下:

//布尔型断言:如果布尔表达式为 false,则抛出 String 类型的异常信息
Assert.isTrue(boolean expression, String message);
    
//对象型断言:如果该对象为 null ,则抛出 String 类型的异常信息
Assert.notNull(@Nullable Object object, String message);

//字符串型断言:如果该字符串无内容,则抛出 String 类型的异常信息
Assert.hasText(@Nullable String text, String message)
    
//集合型断言:如果集合(包括 Map)对象无内容,则抛出 String 类型的异常信息
Assert.notEmpty(@Nullable Map<?, ?> map, String message);
Assert.notEmpty(@Nullable Collection<?> collection, String message);


文章小结

作为一个系列文章的开头,本文的内容偏基础。在之后的
文章中我会分享一些关于真实项目中关于线上 bug 处理、缓存的使用、异步/解耦等内容,敬请期待。

那么 Java 实际开发中值得注意的几个小技巧的分享到这里就暂时结束了,如有不足和错误,还请大家指正。或者你有其它想说的,也欢迎大家在评论区交流!