0%

1、简介

1.1、什么是Mybatis

  • MyBatis 是一款优秀的持久层框架。
  • 它支持自定义 SQL、存储过程以及高级映射。
  • MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。
  • MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
  • MyBatis 本是apache的一个开源项目 iBatis。
  • 2010年这个项目由 apache software foundation 迁移到了 google code,并且改名为MyBatis。
  • 2013年11月迁移到 Github。

如何获取 Mybatis?

阅读全文 »

在训练模型的过程中,过拟合几乎是不可避免的。因此可以这么说,深度学习就是训练一个庞大的模型,在此基础上我们来降低过拟合的程度。相较之下,如果模型欠拟合了,貌似除了扩大模型就没有任何方法了。

下面介绍几个常用的降低过拟合的方法。

一、正则化

1. 什么是正则化?

正则化的思想,即通过限制参数值的选择范围来控制模型容量。而正则化又分为岭回归(权重衰退)、LASSO 回归和弹性网络等。下面我着重说明的是岭回归。

首先,先来看一张极度过拟合的图像。

这是上述图像的部分权重,无一例外,每个 $W_i$ 都极其的大,这也导致了图像十分的陡峭

1
2
3
4
5
6
7
array([-4.09493627e+11,  5.76349998e+12,  1.71369179e+11, -9.99031499e+12,
9.23500127e+11, 9.26094018e+12, -1.94635409e+12, -5.40895631e+10,
7.94601628e+11, -7.85418293e+12, 1.63904594e+12, 1.67984971e+12,
-9.87156668e+11, 6.88721582e+12, -1.64914180e+12, 3.50775793e+11,
2.60751888e+11, -5.87372086e+12, 1.66748622e+12, -3.77434047e+12,
1.00605169e+12, 2.34190394e+12, -8.57867266e+11, 5.39077331e+12,
-1.60621032e+12, 2.95930952e+12, -9.52432067e+11, -1.74889800e+12])

因此很自然而然地就能想到,那我限制 $W_i$ 的选择范围就行了嘛。从模型的角度来说,参数数量不变,但参数的选择范围小了,那模型自然也变小了。

于是就有了使用均方范数作为硬性限制,小的 θ 意味着更强的正则项。

需要注意的是,偏置 b 并没有加入到正则化中来,毕竟我们的目标是让曲线更加的平缓,跟偏置 b 没有什么关系。

$min l(w,b) \quad subject\ to \quad ||W||^2 \le θ,\quad\quad ||W||^2 = \sum W^2$

但硬性限制优化求导比较麻烦,结果也会比较,一般使用均方范数作为柔性限制。

$loss = l(w, b) + \frac{λ}{2}||W||^2$

其中超参数 λ 控制了正则项的重要程度

  • λ = 0,即无正则化,和普通的损失函数没有区别。
  • λ → $\infty$,此时 W → 0

2. 如何影响损失函数?

可以看到,原先的极值点 $\widetilde{W}^*$ 在绿色椭圆的圆心,但在加入正则化项之后,极值点在两者之间做了一个权衡,取在了切点。

3. 参数更新法则

  • 计算梯度

    $\frac{\partial}{\partial W}(l(W, b) + \frac{λ}{2}||W||^2) = \frac{\partial l(W, b)}{\partial W} + λW$

  • 更新参数

    $W’ = W - η(\frac{\partial l(W, b)}{\partial W} + λW) = (1 - ηλ)W - η\frac{\partial l(W, b)}{\partial W}$

通常 ηλ < 1,因此每次在参数更新时,都会对 W 进行缩小,也就是权重衰退这个名字的由来。

4. 岭回归

1
2
3
4
5
6
7
def RidgeRegression(degree,alpha):
pipeline = Pipeline([
("poly",PolynomialFeatures(degree = degree)),
("std_scaler",StandardScaler()),
("ridge_reg",Ridge(alpha=alpha))
])
return pipeline

alpha = 0,即普通多项式回归。

alpha = 1e-4,曲线一下子就柔和了。

alpha = 100,有点像二次曲线了。

alpha → $\infty$,为了控制损失函数,只能将权重设为0。

5. Tensorflow 岭回归实现

为了能够过拟合,只设置了20个训练样本,每个样本有 200 个特征。

1
2
3
4
5
6
7
8
9
10
11
n_train = 20
n_test = 100
num_inputs = 200
batch_size = 5
num_outputs = 1

true_w, true_b = tf.ones([num_inputs, 1]) * 0.01, 0.05
train_data = d2l.synthetic_data(true_w, true_b, n_train)
test_data = d2l.synthetic_data(true_w, true_b, n_test)
train_iter = d2l.load_array(train_data, batch_size)
test_iter = d2l.load_array(test_data, batch_size, is_train=False)

参数初始化,线性模型没那么多讲究,初始化为0也行。

1
2
3
4
5
6
def init_params(num_inputs, num_outputs):
W = tf.Variable(tf.random.normal(mean=1, shape=(num_inputs, 1)))
b = tf.Variable(tf.zeros(num_outputs))
return [W, b]

W, b = init_params(num_inputs, num_outputs)

网络模型

1
2
3
4
5
6
7
8
9
10
11
# 线性模型
def net(X):
return X @ W + b

# L2正则化
def l2_penalty(W):
return tf.reduce_sum(tf.pow(W, 2)) / 2

# MSE损失函数
def loss(y, y_hat):
return tf.reduce_mean(tf.square(y - y_hat))

训练函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def train(lambd, epochs = 100, lr = 0.003):
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
xlim=[5, epochs], legend=['train', 'test'])
for epoch in range(epochs):
for X, y in train_iter:
with tf.GradientTape() as tape:
# 最终损失函数添加正则化项
l = loss(y, net(X)) + lambd * l2_penalty(W)
grads = tape.gradient(l, [W, b])
for param, grad in zip([W, b], grads):
param.assign_sub(grad * lr)
if (epoch + 1) % 5 == 0:
animator.add(epoch +1, (d2l.evaluate_loss(net, train_iter, loss),
(d2l.evaluate_loss(net, test_iter, loss))))
print("W的L2范数是", tf.norm(W).numpy())

train(lambd = 0)

模型没有泛化,光是训练误差减小,典型的过拟合。

train(lambd = 3)

训练和测试误差都在同步降低,且两者差距较上述过拟合之下减小了不少。

train(lambd = 20)

同上,效果更好了。

train(lambd = 100)

虽然图形十分曲折,但总体趋势是在下降且损失更低了。

6. LASSO 回归

和岭回归类似,但正则项使用的是 L1 范数。但绝对值就意味着不可导,不好优化。

$loss = l(w, b) + λ||W||,\quad\quad ||W|| = \sum |W|$

1
2
3
4
5
6
7
8
from sklearn.linear_model import Lasso
def LassoRegression(degree,alpha):
pipeline = Pipeline([
("poly",PolynomialFeatures(degree = degree)),
("std_scaler",StandardScaler()),
("lasso_reg",Lasso(alpha=alpha))
])
return pipeline

alpha = 0

alpha = 0.1

alpha = 1

alpha = 10

通过对比,可以发现一个比较有意思的事情。Ridge 回归在增大 alpha 时,曲线还是弯曲的,但没那么陡峭,因为权重都比较小。而 LASSO 回归在增大 alpha 时,并没有那么多弯曲的地方,因此它的权重大部分都是0。

至于为什么会这样呢?这也和他们正则化项的式子有关。

7. Ridge 和 LASSO 区别

Ridge 回归 的梯度是会随着离极值点越近而渐渐变小的,因此所有的参数是同步在更新,从图像上来看就是沿着梯度慢慢想极值点靠拢,因此不会有很多权重被设为0。

LASSO 回归 的梯度是一个定值,只能由 η 来控制大小,这样就会造成部分权重会早早停在零点,这可以起到一定的特征筛选的作用,虽然也有可能将有用的特征也筛选掉。

8. 弹性网络

顾名思义,是个弹性(折中)的网络,它结合岭回归和 LASSO 回归的思想。

$loss = l(w, b) + γλ||W|| + \frac{(1-γ)}{2}λ||W||^2,\quad\quad γ∈[0,1]$

γ 代表一种比率,取值为 0% ~ 100%,当γ = 0时,该弹性网络为岭回归;当γ = 1时,该弹性网络为 LASSO 回归。

二、丢弃法

1. 什么是丢弃法?

丢弃法,又称 DropOut,具体做法是在每一层输出后,随机将一定量的输出置为0。那么这么做的目的是为什么呢?

一个好的模型需要对输入数据的扰动鲁棒

  • 使用有噪音的数据等价于正则化。
  • 丢弃法则是在层之间加入噪音,同时也降低了模型的容量。

诶,那么这时有人要问了,你这随机置为0,对x的期望都变掉了。为了防止这样的情况,我们不单单是对数据置0,对另一部分的数据也要改动,保证期望不变。

注:p 是一个概率值,将神经元置为0的比率,$p∈[0,1]$。


$$
Ex_i’ = p * X_i * 0 + (1-p) \frac{x_i}{1-p} = x_i
$$
左边没有 Dropout,右边有 Dropout。

2.总结

  • 丢弃法将一些输出项随机置0来控制模型复杂度。
  • 常作用在多层感知机的隐藏层输出上。
  • 丢弃概率是控制模型复杂度的超参数。

3. Tensorflow 实现

dropout层

1
2
3
4
5
6
7
8
9
10
11
def dropout_layer(X, dropout):
assert 0 <= dropout <= 1
if dropout == 1:
return tf.zeros_like(X)
if dropout == 0:
return X

# uniform 均匀分布
mask = tf.random.uniform(X.shape, minval = 0, maxval = 1) < (1 - dropout)
# 不用 X[mask] 是因为乘法运算比匹配运算快
return tf.cast(mask, dtype=tf.float32) * X / (1.0 - dropout)

测试 dropout

1
2
3
4
5
6
# 测试 Dropout
X = tf.reshape(tf.range(16, dtype=tf.float32), [2,8])
print(X)
print(dropout_layer(X, 0))
print(dropout_layer(X, 0.5))
print(dropout_layer(X, 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
26
27
num_outputs = 10
num_hidden1 = 256
num_hidden2 = 256

dropout1 = 0.5
dropout2 = 0.5

class Net(tf.keras.Model):
def __init__(self, num_outputs, num_hidden1, num_hidden2):
super().__init__()
self.input_layer = keras.layers.Flatten()
self.hidden1 = keras.layers.Dense(num_hidden1, activation = "relu")
self.hidden2 = keras.layers.Dense(num_hidden2, activation = "relu")
self.output_layer = keras.layers.Dense(num_outputs, activation = "softmax")

def call(self, inputs, training=None):
X = self.input_layer(inputs)
X = self.hidden1(X)
if training:
X = dropout_layer(X, dropout1)
X = self.hidden2(X)
if training:
X = dropout_layer(X, dropout2)
X = self.output_layer(X)
return X

net = Net(num_outputs, num_hidden1, num_hidden2)

训练

这里说明一下损失函数的使用情况,我之前也一直没有注意过。

  • SparseCategoricalCrossentropy 会给 label 做一个 one-hot 编码。
  • CategoricalCrossentropy 不会给 label做 one-hot 编码。
  • from_logits = True 用于最后输出层没有经过 softmax 的情况,会给结果补做一个 softmax。
1
2
3
4
5
6
7
8
epochs = 10
lr = 0.5
batch_size = 256
# 若输出没有经过 softmax,需使用 keras.losses.SparseCategoricalCrossentropy(from_logits=True)
loss = keras.losses.SparseCategoricalCrossentropy()
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
trainer = keras.optimizers.SGD(learning_rate = lr)
d2l.train_ch3(net, train_iter, test_iter, loss, epochs, trainer)

当我们在训练一个神经网络的时候,参数的随机初始化是非常重要的,对于逻辑回归来说,可以将权重初始化为0。而对于神经网络而言,这样做将会导致梯度下降算法无法起作用。

1. 为什么适用于逻辑回归?

如下图所示,其中$X_1$ 和 $X_2$ 是特征值。

前向传播

$a_1 = sigmoid(X_1 * W_1 + X_2 * W_2 + b)$

$loss = -ylog(a_1) - (1 - y)log(1 - a_1)$

反向传播

$da_1 = -\frac{y}{a_1} + \frac{1 - y}{1 - a_1}$

$dw_1 = da_1 * a’_1 * X_1 = (a_1 - y) * X_1$

$dw_2 = da_1 * a’_1 * X_2 = (a_1 - y) * X_2$

$db = da_1 * a’_1 * 1 = a_1 - y$

参数更新

$W_1 = w_1 - η * dw_1$

$W_2 = w_2 - η * dw_2$

$b = b - η * db$

可以看到, $W_1$ 和 $W_2$ 并不影响 $dw_1$ 和 $dw_2$ 的值,而是根据 $X_1$ 和 $X_2$ 的不同而改变,且不为0,模型的权重能够得到更新。因此即使我们将 $W_1$ 和 $W_2$ 初始化为0也无所谓。参数 同理。

2. 为什么不适用于神经网络?

神经网络结构图如下。

前向传播

$a_1 = f(X_1 * W_{11} + X_2 * W_{21} + b_1)$

$a_2 = f(X_1 * W_{12} + X_2 * W_{22} + b_2)$

$a_3 = sigmoid(a_1 * W_{13} + a_2 * W_{23} + b_3)$

$loss = -ylog(a_3) - (1 - y)log(1 - a_3)$

反向传播

$da_3 = -\frac{y}{a_3} + \frac{1 - y}{1 - a_3}$

$dw_{13} = da_3 * a’_3 * a_1 = (a_3 - y) * a_1$

$dw_{23} = da_3 * a’_3 * a_2 = (a_3 - y) * a_2$

$db_{3} = da_3 * a’_3 * 1 = a_3 - y$

$da_1 = da_3 * a’3 * W{13} = (a_3 - y) * W_{13}$

$da_2 = da_3 * a’3 * W{23} = (a_3 - y) * W_{23}$

$dw_{12} = da_2 * a’_2 * X_1$

$dw_{22} = da_2 * a’_2 * X_2$

$db_{2} = da_2 * a’_2$

$dw_{11} = da_1 * a’_1 * X_1$

$dw_{21} = da_1 * a’_1 * X_2$

$db_{1} = da_1 * a’_1$

参数更新

$W_1 = w_1 - η * dw_1$

$W_2 = w_2 - η * dw_2$

$b = b - η * db$

根据上述的详细公式,我们分析一下3种情况:

  • 模型所有权重 W 初始化为0,所有偏置 b 初始化为0
  • 模型所有权重 W 初始化为0,所有偏置 b 随机初始化
  • 模型所有的权重 W 随机初始化,所有偏置 b 初始化为0

2.1 模型所有权重 W 初始化为0,所有偏置 b 初始化为0

在此情况下, 第一个 batch 的前向传播过程时,$a_1 = f(0), a_2 = f(0), a_3 = sigmoid(0)$。在反向传播进行参数更新的时候,会发现 $a_1 = a_2 = f(0) \ \ =>\ \ dw_{13} = dw_{23}$,$W_{13} = W_{23} = 0 \ \ => \ \ da_1 = da_2 = 0$。也就是说,在第一个 batch 中,只有 $W_{13}$ 和 ${W_{23}}$ 进行了更新并且相等,而其它参数均没有更新。

而当第二个 batch 传给神经网络时,$W_{13} = W_{23} \neq 0 \ \ =>\ \ da_1 = da_2 \ \ => dw_{21} = dw_{22},\ dw_{11} = dw_{12}$。

以此类推,无论训练多少次,无论隐藏层神经元有多少个,由于权重的对称性,隐藏层神经单元的输出始终不变(权重相等)。我们希望不同的神经元能够有不同的输出,这样的神经网络才有意义。

总结:将权重 W 初始化为0,会导致同一隐藏层的所有神经元输出都一致。对于后期不同的 batch,每一隐藏层的权重都能得到更新,但是每一隐藏层神经元的权重都是一致的,多个隐藏神经元的作用就如同1个神经元。

2.2 模型所有权重 W 初始化为0,所有偏置 b 随机初始化

在此情况下,第一个 batch 的前向传播过程时,$a_1 = f(b_1), a_2 = f(b_2), a_3 = sigmoid(b_3)$,在反向传播过程时,$da_1 = da_2 = 0 \ \ => \ \ dw_{11} = dw_{12} = dw_{21} = dw_{22} = 0$,因此第一个 batch 中只有 $W_{13}, W_{23}$ 和 $B_{3}$ 能得到更新。

同理,在第二个 batch 反向传播的过程中,由于 $W_{13}$ 和 $W_{23}$ 不为0,因此所有的参数都能得到更新。这种方式存在更新较慢、梯度消失、梯度爆炸等问题,在实践中,通常不会选择该方法。

2.3 模型所有的权重 W 随机初始化,所有偏置 b 初始化为0

在此情况下,第一个 batch 的前向传播过程时,由于 $W_{13}$ 和 $W_{23}$ 不为0,因此所有的参数可以直接得到更新。

结论:在训练神经网络的时候,权重初始化要谨慎,不能初始化为0!

1、注解

1.1、 内置注解

  • @Deprecated

    被注解的元素是不鼓励使用的程序元素,通常是因为它是危险的,或者因为存在更好的替代方法。

  • @Override

    表示重写父类方法。

  • @SuppressWarning

    抑制警告。

  • @FunctionaInterface

    指定接口必须为函数式接口。

1.2、元注解

元注解的作用就是负责注解其它注解。

  • @Target

    用于描述注解的使用范围。

  • @Retention

    表示需要在什么级别保存该注释信息,用于描述注解的生命周期。

    • SOURCE < CLASS < RUNTIME
  • @Document

    说明该注解将被包含在 javadoc 中。

  • @Inherited

    说明子类可以继承父类中的该注释。

1.3、自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@interface MyAnnotation1{
int age();
String[] hobby() default {"dance", "sing"};
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation2{
String value();
}

注解中的参数一定要加()

当注解只有一个参数时,参数名可以定义为 value,在调用的时候直接赋值就行。

1
2
3
4
5
public class MyAnnotationTest {
@MyAnnotation1(age = 12)
@MyAnnotation2("value")
String name;
}

2、反射

Java 反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性,并且能改变它的属性。

2.1、获取 Class 类的实例

  • 通过类的 class 属性获取,安全可靠,性能最高。

    Class clz = Person.class;

  • 调用实例的 getClass() 方法。

    Class clz = person.getClass();

  • 通过 Class.forName() 获取。

    Class clz = Class.forName("com.yqx.Person");

1
2
3
4
5
6
7
8
@Test
public void test1() throws ClassNotFoundException {
Class clz1 = Person.class;
Class clz2 = new Person().getClass();
Class clz3 = Class.forName("Person");
System.out.println(clz1 == clz2); // true
System.out.println(clz2 == clz3); // true
}

2.2、拥有 Class 对象的类型

  • class:外部类,成员,局部内部类,匿名内部类。
  • interface:接口
  • []:数组
  • enum:枚举
  • annotation:注解 @interface
  • primitive type:基本数据类型
  • void
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
@Test
public void test2(){
Class clz1 = Object.class;
Class clz2 = Comparable.class;
Class clz3 = String[].class;
Class clz4 = String[][].class;
Class clz5 = Override.class;
Class clz6 = ElementType.class;
Class clz7 = Integer.class;
Class clz8 = void.class;
Class clz9 = Class.class;

System.out.println(clz1);
System.out.println(clz2);
System.out.println(clz3);
System.out.println(clz4);
System.out.println(clz5);
System.out.println(clz6);
System.out.println(clz7);
System.out.println(clz8);
System.out.println(clz9);

int[] a = new int[10];
int[] b = new int[100];
System.out.println(a.getClass() == b.getClass());
}

打印输出 (数组即使长度不同,其 Class 类也都是相同的)

1
2
3
4
5
6
7
8
9
10
11
12
class java.lang.Object
interface java.lang.Comparable
class [Ljava.lang.String;
class [[Ljava.lang.String;
interface java.lang.Override
class java.lang.annotation.ElementType
class java.lang.Integer
void
class java.lang.Class
true

Process finished with exit code 0

2.3、类的加载

2.4、获取类的属性及方法

反射机制允许程序在运行时取得任何一个已知名称的 class 的内部信息,包括包括其modifiers (修饰符),fields (属性),methods (方法)等,并可于运行时改变 fields 内容或调用 methods。

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
@Test
public void test3() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
// 获取 Class 对象
Class c1 = Class.forName("Person");

// 创建对象(无参)
Person person1 = (Person) c1.newInstance();
System.out.println(person1);

// 通过构造器创建对象
Constructor constructor = c1.getDeclaredConstructor(String.class, int.class);
Person person2 = (Person) constructor.newInstance("yqx", 20);
System.out.println(person2);

// 反射调用方法
Method method1 = c1.getDeclaredMethod("getName");
Method method2 = c1.getDeclaredMethod("setName", String.class);
String name1 = (String) method1.invoke(person1);
method2.invoke(person1, "deflory");
String name2 = (String) method1.invoke(person1);
System.out.println("Before: " + name1);
System.out.println("After: " + name2);

// 反射操作属性
Field field = c1.getDeclaredField("age");
field.setAccessible(true); // 设置可访问的
field.set(person1, 18);
System.out.println(person1.getAge());
}

打印内容

1
2
3
4
5
6
7
Person{name='null', age=0}
Person{name='yqx', age=20}
Before: null
After: deflory
18

Process finished with exit code 0

2.5、性能测试

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
@Test
public void test4() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 普通方法
Person person = new Person();
long start = System.currentTimeMillis();
for(int i=0;i<1e9;i++){
person.getAge();
}
long end = System.currentTimeMillis();
System.out.println("普通方法用时:" + (end - start) + "ms");

// 反射(检测开启)
Method method = Person.class.getDeclaredMethod("getAge");
start = System.currentTimeMillis();
for(int i=0;i<1e9;i++){
method.invoke(person);
}
end = System.currentTimeMillis();
System.out.println("反射(检测开启)用时:" + (end - start) + "ms");

// 反射(检测关闭)
method.setAccessible(true);
start = System.currentTimeMillis();
for(int i=0;i<1e9;i++){
method.invoke(person);
}
end = System.currentTimeMillis();
System.out.println("反射(检测关闭)用时:" + (end - start) + "ms");
}

打印内容

1
2
3
4
5
普通方法用时:1379ms
反射(检测开启)用时:1863ms
反射(检测关闭)用时:1591ms

Process finished with exit code 0

2.6、反射操作泛型

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
@Test
public void test5() throws NoSuchMethodException {
Method test01 = Person.class.getDeclaredMethod("test01", Map.class, List.class);
Method test02 = Person.class.getDeclaredMethod("test02");

System.out.println("test01(参数泛型)");
Type[] genericParameterTypes = test01.getGenericParameterTypes();
for(Type genericParameterType : genericParameterTypes){
System.out.println(genericParameterType);
if (genericParameterType instanceof ParameterizedType){
Type[] actualTypeArguments = ((ParameterizedType) genericParameterType).getActualTypeArguments();
for(Type actualTypeArgument : actualTypeArguments){
System.out.println(actualTypeArgument);
}
}
}

System.out.println("\ntest02(返回值泛型)");
Type getGenericReturnType = test02.getGenericReturnType();
System.out.println(getGenericReturnType);
if (getGenericReturnType instanceof ParameterizedType){
Type[] actualTypeArguments = ((ParameterizedType) getGenericReturnType).getActualTypeArguments();
for(Type actualTypeArgument : actualTypeArguments){
System.out.println(actualTypeArgument);
}
}
}

打印内容

1
2
3
4
5
6
7
8
9
10
11
12
13
test01(参数泛型)
java.util.Map<java.lang.String, java.lang.Integer>
class java.lang.String
class java.lang.Integer
java.util.List<java.lang.Character>
class java.lang.Character

test02(返回值泛型)
java.util.Map<java.lang.String, java.lang.Integer>
class java.lang.String
class java.lang.Integer

Process finished with exit code