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

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


  • 首页

  • 归档

  • 搜索

JAVA Stream用法

发表于 2017-12-13

Stream(流)在JAVA已经不是一个新词了。很早之前我们就接触过JAVA中的输入输出流(IO Stream),它是对数据输入输出操作的抽象封装。JAVA8中提出一个集合流的抽象工具(java.util.stream,简称Stream),用于集合内元素的计算,更确切的说是过滤和统计操作。

Stream创建

Stream不是一种真实的数据源(不存在数据结构),所以我们没有办法直接来创建它,Stream只能依赖其他数据源来转换成我们的抽象操作。Stream本身是不存在,只是我们抽象出来的一个抽象操作,经过各种操作之后,Stream还需要转换成真实的数据源。

常见创建方式如下:

  • Collection

parallelStream()

stream()

  • Stream.of(…)
  • Arrays.stream(…)
  • Stream.generate(…)
  • Stream.iterate(seek, unaryOperator)
  • BufferedReader

lines()

其实最终都是依赖StreamSupport类来完成Stream创建的。

Stream操作

To perform a computation, stream operations are composed into a stream pipeline. A stream pipeline consists of a source (which might be an array, a collection, a generator function, an I/O channel, etc), zero or more intermediate operations (which
transform a stream into another stream, such as filter(Predicate)), and a terminal operation (which produces a result or side-effect, such as count() or forEach(Consumer)). Streams are lazy; computation on the source data is only performed
when the terminal operation is initiated, and source elements are consumed only as needed.

Stream操作由零个或多个中间操作(intermediate operation)和一个结束操作(terminal operation)两部分组成。只有执行结束操作时,Stream定义的中间操作才会依次执行,这就是Stream的延迟特性。

中间操作

  • filter

Returns a stream consisting of the elements of this stream that match the given predicate.

返回一个符合条件的Stream。

  • map

Returns a stream consisting of the results of applying the given function to the elements of this stream.

返回由新元素组成的Stream。

  • mapToInt、mapToLong、mapToDouble

返回int、long、double基本类型对应的Stream。

  • flatMap

Returns a stream consisting of the results of replacing each element of this stream with the contents of a mapped stream produced by applying the provided mapping function to each element. Each mapped stream is closed after its contents have been
placed into this stream. (If a mapped stream is null an empty stream is used, instead.)

简单的说,就是一个或多个流合并成一个新流。

  • flatMapToInt、flatMapToLong、flatMapToDouble

返回对应的IntStream、LongStream、DoubleStream流。

  • distinct

返回去重的Stream。

  • sorted

返回一个排序的Stream。

  • peek

主要用来查看流中元素的数据状态。

  • limit

返回前n个元素数据组成的Stream。属于短路操作

  • skip

返回第n个元素后面数据组成的Stream。

结束操作

  • forEach

循环操作Stream中数据。

  • forEachOrdered

暗元素顺序执行循环操作。

  • toArray

返回流中元素对应的数组对象。

  • reduce

聚合操作,用来做统计。

  • collect

聚合操作,封装目标数据。

  • min、max、count

聚合操作,最小值,最大值,总数量。

  • anyMatch

短路操作,有一个符合条件返回true。

  • allMatch

所有数据都符合条件返回true。

  • noneMatch

所有数据都不符合条件返回true。

  • findFirst

短路操作,获取第一个元素。

  • findAny

短路操作,获取任一元素。

总结

  • Stream每个操作都是依赖Lambda表达式或方法引用。
  • Stream操作是一种声明式的数据处理方式。
  • Stream操作提高了数据处理效率、开发效率。

本文转载自: 掘金

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

BP算法从原理到python实现 BP算法从原理到实践 反向

发表于 2017-12-13

BP算法从原理到实践

反向传播算法Backpropagation的python实现

觉得有用的话,欢迎一起讨论相互学习~Follow Me

博主接触深度学习已经一段时间,近期在与别人进行讨论时,发现自己对于反向传播算法理解的并不是十分的透彻,现在想通过这篇博文缕清一下思路.自身才疏学浅欢迎各位批评指正.
参考文献
李宏毅深度学习视频
The original location of the code

关于反向传播算法的用途在此不再赘述,这篇博文主要是理解形象化理解反向传播算法与python进行实践的.

反向传播是一种有效率进行梯度下降的方法

  • 在神经网络中,我们往往有很多参数,每一个神经元与另一个神经元的连接都有一个权重(weight),每一个神经元都有一个偏置(bias).在梯度下降减小loss function的值时我们会对所有的参数进行更新.
  • 我们设所有的参数为θ
    θ ,初始化的θ
    θ 记为θ 0
    θ 0 .其经过梯度下降后的取值设为θ 1 ,θ 2 ,θ 3 .. .θn
    θ 1 ,θ 2 ,θ 3 .. .θ n
  • η
    η 表示学习率,L(θ)
    L (θ) 表示Lossfunction,∇
    ∇ 表示梯度.

2017-12-07_103551

  • 假设我们需要做语音辨识,有7-8层神经层,每层有1000个神经元,这时我们的梯度向量∇ L(θ)
    ∇ L(θ) 是一个有上百万维度的向量,这时候我们使用反向传播算法有效率的计算参数的梯度下降值.

BP算法数学原理直观表示

Chain Rule 链式法则

IMG_20171207_112145

  • 我们将训练数据的正确值(理想值)称为ˆ y
    y ^ 而把模型的实际输出值记作y
    y .Cost function是对于一个训练数据y 和ˆ y 距离 的函 数 .则Lost function是所有训练数据的Cost function值的加和.
  • 即若我们想计算Loss function对w的偏导数,只要计算训练集上所有训练数据对w的偏导数之和即可.

L(θ) =N ∑ n= 1 Cn (θ)

∂ L(θ ) ∂ w =N ∑ n= 1 ∂C n (θ)∂ w
Backpropagation 实现


  • 假设我们取一个简单神经网络的输入层作考虑.

Forward pass前向传播

  • 对于前向传播,∂ z ∂ wn =X n [即前向传播中的连接输入值(也是连接中上一个神经元的输出值)即是激活函数对该边权值的偏导数]

也就是说只要我们算出每一个神经元的输出就能知道与其相连的边的cost function 对权值的偏微分.

Backward pass反向传播

IMG_20171211_005837

对于此处的 ∂ C ∂ z′ 和∂ C ∂ z″ 是结 构十 分复 杂的 表达 式在 接下 来我 们会 进行 计算 ,现 在假 设其 已经 计算 出来

则此时有∂ C ∂ z =σ ′ (z) [w3 ∂C ∂ z′ +w 4 ∂C ∂ z″ ]

此时我们注意到,要计算∂ C ∂ z 我们除了需要这个神经元的输出之外,还需要知道和这个神经元连接的神经元的所有权值和cost function对于这些神经元输入值的偏导

case1 output layer

  • 假设此神经元已经是最后一层隐藏层神经元,其后连接的是输出层,有输出y1和y2.

IMG_20171211_025442

case2 not output layer

  • 假设此神经元不是最后一层隐藏层的神经元,即其后还有许多层的神经元.

2017-12-11_025959

2017-12-11_034337

  • 根据前面的模型我们知道,如果我们已知∂ C ∂ Za 和∂C∂ Zb 那么 我们 可以 求出 ∂C∂ z′
  • ∂ C ∂ Z′ =( W5 ∗∂ C ∂ Za +W 6 ∗∂ C ∂ Zb )∗ σ′ (Z′)
  • 我们使用这个方法已知持续到最后一层隐藏层,我们可以按照最后一层为隐藏层的方法从后向前进行推导

summary

  • BP算法可以理解为逆向的建立一个神经网络,这个神经网络的激励函数是σ ′ (Z n ) ,这需要通过神经网络的Forward pass前向传播才能得到.
  • 接下来我们可以通过最后一个隐层求得∂ C ∂ Z5 和∂C∂ z6
  • 接下来和普通的神经网络一样对其进行运算求得结果

IMG_20171211_040916

空说无凭,来看代码

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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
复制代码`import random`
import math


#
# Shorthand:
# "pd_" as a variable prefix means "partial derivative"
# "d_" as a variable prefix means "derivative"
# "_wrt_" is shorthand for "with respect to"
# "w_ho" and "w_ih" are the index of weights from hidden to output layer neurons and input to hidden layer neurons respectively
#
# Comment references:
#
# [1] Wikipedia article on Backpropagation
# http://en.wikipedia.org/wiki/Backpropagation#Finding_the_derivative_of_the_error
# [2] Neural Networks for Machine Learning course on Coursera by Geoffrey Hinton
# https://class.coursera.org/neuralnets-2012-001/lecture/39
# [3] The Back Propagation Algorithm
# https://www4.rgu.ac.uk/files/chapter3%20-%20bp.pdf
# [4] The original location of the code
# https://github.com/mattm/simple-neural-network/blob/master/neural-network.py

class NeuralNetwork:
# 神经网络类
LEARNING_RATE = 0.5

# 设置学习率为0.5

def __init__(self, num_inputs, num_hidden, num_outputs, hidden_layer_weights=None, hidden_layer_bias=None,
output_layer_weights=None, output_layer_bias=None):
# 初始化一个三层神经网络结构
self.num_inputs = num_inputs

self.hidden_layer = NeuronLayer(num_hidden, hidden_layer_bias)
self.output_layer = NeuronLayer(num_outputs, output_layer_bias)

self.init_weights_from_inputs_to_hidden_layer_neurons(hidden_layer_weights)
self.init_weights_from_hidden_layer_neurons_to_output_layer_neurons(output_layer_weights)

def init_weights_from_inputs_to_hidden_layer_neurons(self, hidden_layer_weights):
weight_num = 0
for h in range(len(self.hidden_layer.neurons)): # num_hidden,遍历隐藏层
for i in range(self.num_inputs): # 遍历输入层
if not hidden_layer_weights:
# 如果hidden_layer_weights的值为空,则利用随机化函数对其进行赋值,否则利用hidden_layer_weights中的值对其进行更新
self.hidden_layer.neurons[h].weights.append(random.random())
else:
self.hidden_layer.neurons[h].weights.append(hidden_layer_weights[weight_num])
weight_num += 1

def init_weights_from_hidden_layer_neurons_to_output_layer_neurons(self, output_layer_weights):
weight_num = 0
for o in range(len(self.output_layer.neurons)): # num_outputs,遍历输出层
for h in range(len(self.hidden_layer.neurons)): # 遍历输出层
if not output_layer_weights:
# 如果output_layer_weights的值为空,则利用随机化函数对其进行赋值,否则利用output_layer_weights中的值对其进行更新
self.output_layer.neurons[o].weights.append(random.random())
else:
self.output_layer.neurons[o].weights.append(output_layer_weights[weight_num])
weight_num += 1

def inspect(self): # 输出神经网络信息
print('------')
print('* Inputs: {}'.format(self.num_inputs))
print('------')
print('Hidden Layer')
self.hidden_layer.inspect()
print('------')
print('* Output Layer')
self.output_layer.inspect()
print('------')

def feed_forward(self, inputs): # 返回输出层y值
hidden_layer_outputs = self.hidden_layer.feed_forward(inputs)
return self.output_layer.feed_forward(hidden_layer_outputs)

# Uses online learning, ie updating the weights after each training case
# 使用在线学习方式,训练每个实例之后对权值进行更新
def train(self, training_inputs, training_outputs):
self.feed_forward(training_inputs)
# 反向传播
# 1. Output neuron deltas输出层deltas
pd_errors_wrt_output_neuron_total_net_input = [0]*len(self.output_layer.neurons)
for o in range(len(self.output_layer.neurons)):
# 对于输出层∂E/∂zⱼ=∂E/∂a*∂a/∂z=cost'(target_output)*sigma'(z)
pd_errors_wrt_output_neuron_total_net_input[o] = self.output_layer.neurons[
o].calculate_pd_error_wrt_total_net_input(training_outputs[o])

# 2. Hidden neuron deltas隐藏层deltas
pd_errors_wrt_hidden_neuron_total_net_input = [0]*len(self.hidden_layer.neurons)
for h in range(len(self.hidden_layer.neurons)):
# We need to calculate the derivative of the error with respect to the output of each hidden layer neuron
# 我们需要计算误差对每个隐藏层神经元的输出的导数,由于不是输出层所以dE/dyⱼ需要根据下一层反向进行计算,即根据输出层的函数进行计算
# dE/dyⱼ = Σ ∂E/∂zⱼ * ∂z/∂yⱼ = Σ ∂E/∂zⱼ * wᵢⱼ
d_error_wrt_hidden_neuron_output = 0
for o in range(len(self.output_layer.neurons)):
d_error_wrt_hidden_neuron_output += pd_errors_wrt_output_neuron_total_net_input[o]* \
self.output_layer.neurons[o].weights[h]

# ∂E/∂zⱼ = dE/dyⱼ * ∂zⱼ/∂
pd_errors_wrt_hidden_neuron_total_net_input[h] = d_error_wrt_hidden_neuron_output*self.hidden_layer.neurons[
h].calculate_pd_total_net_input_wrt_input()

# 3. Update output neuron weights 更新输出层权重
for o in range(len(self.output_layer.neurons)):
for w_ho in range(len(self.output_layer.neurons[o].weights)):
# 注意:输出层权重是隐藏层神经元与输出层神经元连接的权重
# ∂Eⱼ/∂wᵢⱼ = ∂E/∂zⱼ * ∂zⱼ/∂wᵢⱼ
pd_error_wrt_weight = pd_errors_wrt_output_neuron_total_net_input[o]*self.output_layer.neurons[
o].calculate_pd_total_net_input_wrt_weight(w_ho)

# Δw = α * ∂Eⱼ/∂wᵢ
self.output_layer.neurons[o].weights[w_ho] -= self.LEARNING_RATE*pd_error_wrt_weight

# 4. Update hidden neuron weights 更新隐藏层权重
for h in range(len(self.hidden_layer.neurons)):
for w_ih in range(len(self.hidden_layer.neurons[h].weights)):
# 注意:隐藏层权重是输入层神经元与隐藏层神经元连接的权重
# ∂Eⱼ/∂wᵢ = ∂E/∂zⱼ * ∂zⱼ/∂wᵢ
pd_error_wrt_weight = pd_errors_wrt_hidden_neuron_total_net_input[h]*self.hidden_layer.neurons[
h].calculate_pd_total_net_input_wrt_weight(w_ih)

# Δw = α * ∂Eⱼ/∂wᵢ
self.hidden_layer.neurons[h].weights[w_ih] -= self.LEARNING_RATE*pd_error_wrt_weight

def calculate_total_error(self, training_sets):
# 使用平方差计算训练集误差
total_error = 0
for t in range(len(training_sets)):
training_inputs, training_outputs = training_sets[t]
self.feed_forward(training_inputs)
for o in range(len(training_outputs)):
total_error += self.output_layer.neurons[o].calculate_error(training_outputs[o])
return total_error


class NeuronLayer:
# 神经层类
def __init__(self, num_neurons, bias):

# Every neuron in a layer shares the same bias 一层中的所有神经元共享一个bias
self.bias = bias if bias else random.random()
random.random()
# 生成0和1之间的随机浮点数float,它其实是一个隐藏的random.Random类的实例的random方法。
# random.random()和random.Random().random()作用是一样的。
self.neurons = []
for i in range(num_neurons):
self.neurons.append(Neuron(self.bias))
# 在神经层的初始化函数中对每一层的bias赋值,利用神经元的init函数对神经元的bias赋值

def inspect(self):
# print该层神经元的信息
print('Neurons:', len(self.neurons))
for n in range(len(self.neurons)):
print(' Neuron', n)
for w in range(len(self.neurons[n].weights)):
print(' Weight:', self.neurons[n].weights[w])
print(' Bias:', self.bias)

def feed_forward(self, inputs):
# 前向传播过程outputs中存储的是该层每个神经元的y/a的值(经过神经元激活函数的值有时被称为y有时被称为a)
outputs = []
for neuron in self.neurons:
outputs.append(neuron.calculate_output(inputs))
return outputs

def get_outputs(self):
outputs = []
for neuron in self.neurons:
outputs.append(neuron.output)
return outputs


class Neuron:
# 神经元类
def __init__(self, bias):
self.bias = bias
self.weights = []

def calculate_output(self, inputs):
self.inputs = inputs
self.output = self.squash(self.calculate_total_net_input())
# output即为输入即为y(a)意为从激活函数中的到的值
return self.output

def calculate_total_net_input(self):
# 此处计算的为激活函数的输入值即z=W(n)x+b
total = 0
for i in range(len(self.inputs)):
total += self.inputs[i]*self.weights[i]
return total + self.bias

# Apply the logistic function to squash the output of the neuron
# 使用sigmoid函数为激励函数,一下是sigmoid函数的定义
# The result is sometimes referred to as 'net' [2] or 'net' [1]
def squash(self, total_net_input):
return 1/(1 + math.exp(-total_net_input))

# Determine how much the neuron's total input has to change to move closer to the expected output
# 确定神经元的总输入需要改变多少,以接近预期的输出
# Now that we have the partial derivative of the error(Cost function) with respect to the output (∂E/∂yⱼ)
# 我们可以根据cost function对y(a)神经元激活函数输出值的偏导数和激活函数输出值y(a)对激活函数输入值z=wx+b的偏导数计算delta(δ).
# the derivative of the output with respect to the total net input (dyⱼ/dzⱼ) we can calculate
# the partial derivative of the error with respect to the total net input.
# This value is also known as the delta (δ) [1]
# δ = ∂E/∂zⱼ = ∂E/∂yⱼ * dyⱼ/dzⱼ 关键key
#
def calculate_pd_error_wrt_total_net_input(self, target_output):
return self.calculate_pd_error_wrt_output(target_output)*self.calculate_pd_total_net_input_wrt_input()

# The error for each neuron is calculated by the Mean Square Error method:
# 每个神经元的误差由平均平方误差法计算
def calculate_error(self, target_output):
return 0.5*(target_output - self.output) ** 2

# The partial derivate of the error with respect to actual output then is calculated by:
# 对实际输出的误差的偏导是通过计算得到的--即self.output(y也常常用a表示经过激活函数后的值)
# = 2 * 0.5 * (target output - actual output) ^ (2 - 1) * -1
# = -(target output - actual output)BP算法最后隐层求cost function 对a求导
#
# The Wikipedia article on backpropagation [1] simplifies to the following, but most other learning material does not [2]
# 维基百科关于反向传播[1]的文章简化了以下内容,但大多数其他学习材料并没有简化这个过程[2]
# = actual output - target output
#
# Note that the actual output of the output neuron is often written as yⱼ and target output as tⱼ so:
# = ∂E/∂yⱼ = -(tⱼ - yⱼ)
# 注意我们一般将输出层神经元的输出为yⱼ,而目标标签(正确答案)表示为tⱼ.
def calculate_pd_error_wrt_output(self, target_output):
return -(target_output - self.output)

# The total net input into the neuron is squashed using logistic function to calculate the neuron's output:
# yⱼ = φ = 1 / (1 + e^(-zⱼ)) 注意我们对于神经元使用的激活函数都是logistic函数
# Note that where ⱼ represents the output of the neurons in whatever layer we're looking at and ᵢ represents the layer below it
# 注意我们用j表示我们正在看的这层神经元的输出,我们用i表示这层的后一层的神经元.
# The derivative (not partial derivative since there is only one variable) of the output then is:
# dyⱼ/dzⱼ = yⱼ * (1 - yⱼ)这是sigmoid函数的导数表现形式.
def calculate_pd_total_net_input_wrt_input(self):
return self.output*(1 - self.output)

# The total net input is the weighted sum of all the inputs to the neuron and their respective weights:
# 激活函数的输入是所有输入的加权权重的总和
# = zⱼ = netⱼ = x₁w₁ + x₂w₂ ...
# The partial derivative of the total net input with respective to a given weight (with everything else held constant) then is:
# 总的净输入与给定的权重的偏导数(其他所有的项都保持不变)
# = ∂zⱼ/∂wᵢ = some constant + 1 * xᵢw₁^(1-0) + some constant ... = xᵢ
def calculate_pd_total_net_input_wrt_weight(self, index):
return self.inputs[index]


# Blog post example:

nn = NeuralNetwork(2, 2, 2, hidden_layer_weights=[0.15, 0.2, 0.25, 0.3], hidden_layer_bias=0.35,
output_layer_weights=[0.4, 0.45, 0.5, 0.55], output_layer_bias=0.6)
for i in range(10000):
nn.train([0.05, 0.1], [0.01, 0.99])
print(i, round(nn.calculate_total_error([[[0.05, 0.1], [0.01, 0.99]]]), 9)) # 截断处理只保留小数点后9位

# XOR example:

# training_sets = [
# [[0, 0], [0]],
# [[0, 1], [1]],
# [[1, 0], [1]],
# [[1, 1], [0]]
# ]

# nn = NeuralNetwork(len(training_sets[0][0]), 5, len(training_sets[0][1]))
# for i in range(10000):
# training_inputs, training_outputs = random.choice(training_sets)
# nn.train(training_inputs, training_outputs)
# print(i, nn.calculate_total_error(training_sets))

本文转载自: 掘金

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

如何让异步接口同时支持 callback 和 promise

发表于 2017-12-13

避免 unhandledRejection 事件

随着 ES6 的普及,越来越多的异步接口都开始同时支持callback和promise两种方式,我在最近的两篇文章《如何用 Node.js 编写一个 API 客户端》和《如何编写一个简单的 Redis 客户端》中也使用一个简单的小技巧来实现这样的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码request(method, path, params, callback) {
return new Promise((_resolve, _reject) => {

const resolve = ret => {
_resolve(ret);
callback && callback(null, ret);
};

const reject = err => {
_reject(err);
callback && callback(err);
};

// 以下部分不变
// ...
});
}

上文的代码使得request()函数可以返回一个Promise对象,同时如果传入了一个callback参数它也能工作良好,这似乎已经能满足了前文的目标。

但这样的做法带来的一个问题是,如果我们使用callback方式,当request()函数在执行时回调了一个错误对象(即执行了callback(err)和reject(err)),此时会触发一个unhandledRejection事件。大多数情况下这样也并不会影响到我们程序的功能,它还是能够正常的工作,但是这些本该可以避免的unhandledRejection事件会对我们调试程序时造成很大的干扰。

究其原因,正确的实现同时支持callback和promise必须做到,当使用者传入callback参数时不应该返回一个 Promise 对象。如果返回了一个 Promise 对象,而使用者并不会调用.catch()去捕捉可能发生的错误,这样就会导致触发unhandledRejection事件。

所以,针对上文的例子我们可以改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码request(method, path, params, callback) {
if (callback) {
doRequest(method, path, params, callback);
} else {
return new Promise((resolve, reject) => {
doRequest(method, path, params, (err, ret) => {
err ? reject(err) : resolve(ret);
});
});
}

function doRequest(method, path, params, callback) {
// 以下部分不变
// ...
}
}

或者我们可以写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码request(method, path, params, callback) {
if (!callback) {
return new Promise((resolve, reject) => {
// 重新调用当前函数
request(method, path, params, (err, ret) => {
err ? reject(err) : resolve(ret);
});
});
}

// 以下部分不变
// ...
}

重复执行 callback 的坑

也许以上的写法并没有那么直观,我们更希望有这么一个promiseToCallback函数(代码来自《callback和promise的错误捕获
》
],有删改):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码function promiseToCallback(fn) {
return function () {
const args = Array.prototype.slice.apply(arguments);
const callback = args.pop();
fn.apply(null, args)
.then(function (result) {
callback(null, result);
})
.catch(function (err) {
console.error(err);
callback(err);
});
};
}

正如该文章所说的那样,上文这个代码在callback执行出错时,会被.catch()捕捉到,从而又重复执行了一次callback,这样往往会将我们带入一个更大的坑里面。

我们可以通过以下代码来测试这个promiseToCallback()所存在的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码'use strict';

process.on('unhandledRejection', err => {
console.log('unhandledRejection', err);
});

function hello(msg) {
return new Promise((resolve, reject) => {
setImmediate(() => {
resolve(`hello, ${ msg }`);
});
});
}

promiseToCallback(hello)('test', (err, ret) => {
console.log(err, ret);
throw new Error('haha');
});

执行程序后输出结果如下:

1
2
3
4
复制代码null 'hello, test'
[Error: haha]
[Error: haha] undefined
unhandledRejection [Error: haha]

其中第一行的输出是正常回调时的输出,但是在回调里面有抛出了一个haha错误,被promiseToCallback的.catch()捕捉到,然后它先把这个err对象打印出来,再重复执行了一遍回调函数,在回调函数中又输出了一遍。同时,在这次的回调函数中,有抛出了一个haha错误,此时promiseToCallback中的.catch()已经不能再捕捉到这个错误了,然后被注册的unhandledRejection事件监听器监听到,并将其打印了出来。

在此先不讨论这个promiseToCallback()是否满足了同时支持callback和promise这个前提,就重复执行callback的问题我们是万万不能使用它的。

当然我们也可以有办法使得它不会重复执行回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码function promiseToCallback(fn) {
return function () {
const args = Array.prototype.slice.apply(arguments);
const callback = args.pop();

// 包装 callback,在此函数中保证 callback 只会调用一次
// 再次调用会直接忽略
const cb = (err, ret) => {
if (cb.isCalled) return;
cb.isCalled = true;
callback(err, ret);
};

fn.apply(null, args)
.then(function (result) {
cb(null, result);
})
.catch(function (err) {
console.error(err);
cb(err);
});
};
}

我们通过一个isCalled属性来保证了回调函数只会被执行一次,它确实保证了callback不被重复执行,但同时它也悄悄地将callback发生的错误藏了起来,说不定这又成了将来某一天困扰你多时的坑。

也许这是最佳的解决方案

说了这么一大堆,要使得很好地同时支持callback和promise,关键是要处理好这两个问题:

  • 避免unhandledRejection事件(一定要使用promise.catch()捕捉错误)
  • 避免多次执行callback

而我觉得处理好这两个问题其实只需要记住这一个原则:「原始函数使用 callback 实现,仅在必要时才返回 promise」。下面是根据这一原则实现的promiseOrCallback函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码function promiseOrCallback(fn, argc) {
return function () {
const args = Array.prototype.slice.apply(arguments);
// 判断调用函数时实际传过来的参数数量
if (args.length > argc) {
// 这是 callback 方式调用的
return fn.apply(null, args);
}
// 这是 promise 方式调用的
return new Promise((resolve, reject) => {
// 创建一个 callback 函数用来对接 promise 的 resolve 和 reject
args.push((err, ret) => {
err ? reject(err) : resolve(ret);
});
fn.apply(null, args);
});
};
}

说明:在包装函数时,需要明确知道这个函数会接收多少个参数,假设argc = 1,那么当调用包装后的函数时传入了2个参数,则会认为它是以callback方式调用的,否则会返回一个promise。

我们可以使用以下程序来测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码'use strict';

process.on('unhandledRejection', err => {
console.log('unhandledRejection', err);
});

function hello(msg, callback) {
setImmediate(() => {
callback(null, `hello, ${ msg }`);
});
}

promiseOrCallback(hello, 1)('test', (err, ret) => {
console.log(err, ret);
throw new Error('haha');
});

其执行结果应该是这样的:

1
2
3
4
5
6
7
8
9
复制代码null 'hello, test'
/tmp/test.js:45
throw new Error('haha');
^

Error: haha
at /tmp/test.js:45:9
at Immediate._onImmediate (/tmp/test.js:39:5)
at processImmediate [as _immediateCallback] (timers.js:383:17)

说明:在回调函数中,先执行console.log(err, ret)输出了结果,然后throw new Error('haha')再抛出一个错误,这时因为外层没有捕捉到,使得进程因为异常而退出了,这正是我们所期望的。

如果我们改用promise的方式去调用:

1
2
3
4
5
6
复制代码promiseOrCallback(hello, 1)('test').then(ret => {
console.log(null, ret);
throw new Error('haha');
}).catch(err => {
console.log(err);
});

则其执行结果是这样的:

1
2
复制代码null 'hello, test'
[Error: haha]

说明:在.then()的回调函数内,我们先输出结果,在throw出一个错误时,并.catch()捕捉到并打印了出来,这符合promise的行为。

如果你要问「原始函数是基于 promise 实现的,想支持 callback 怎么办」,我建议你最好放弃这个想法。

接口设计的哲学

在本文发出去之后,得到了大神@Hax的点评:

有些代码直接调,不传cb因为它想触发副作用,结果你改成了p,然后还是掉坑了……

结合上下文我们可以理解为,在上文我们通过判断是否传入了一个callback参数来判断异步方式,在合适的时候再返回promise。但是,假如我们仅仅是想触发一个副作用(执行异步函数,但并不关心它的回调结果),由于没有传入callback参数,此时会被自动识别为promise方式调用,于是它返回了一个promise对象。而当函数执行时回调了一个err对象时,我们又重新掉进了前文所说的unhandledRejection的坑里面。

通过判断参数数量这样的方式来实现不同异步方式的转换并不严谨。所以,针对不同的异步方式应该使用不同的接口,比如我们可以规定所有异步方法默认都是callback方式(如request),而promise方式都有P后缀(如requestP)。

@Hax继续评论道:

是的,这是为啥 Node.js 的人最后决定把promise化的 API 单独分开

我个人觉得xxxP名字也不是很友好。其实用不同的包就好了。

1
2
3
4
> 复制代码import xxx from 'api/callback'
> import xxx from 'api/promise'
>
>

所以,封装成单独的包才是更优雅的方式。最后还有一句话,切勿混用callback和promise。

总结

大多数时候,我们只需要一点点小技巧就能使得程序看起来正常地工作起来。然而要写出完美的程序却并不是一件简单的事情。

相关链接

  • callback 和 promise 的错误捕获
  • Promise 陷阱
  • JavaScript Promise 迷你书(中文版)

本文转载自: 掘金

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

Java:关于值传递你需要了解的事情

发表于 2017-12-13

我们都知道,在Java中,方法的参数传递永远都是指值传递。让我们来看一看基本类型和集合的参数传递在内存中是如何体现的。

原文链接:dzone.com/articles/ja…

在讨论Java中参数是如何传递之前,我们有必要先弄清楚Java的变量(主要指的是基本类型和对象)是怎么存储在内存中的。
基本类型一般都存储在堆栈中;对于Java对象,实际的对象数据存储在堆中,而对象的指针(指向推中的对象)存储在堆栈中。

1.png

1.传值 vs 传引用

“传值”和“传引用”分别是什么意思:

  • 传值:当方法参数是值传递时,意味着原参数的一个拷贝被传到了参数内部而不是原始参数,所以任何对于该参数的改变都只会影响这个拷贝值。
  • 传引用:当方法参数是引用传递时,意味着原始参数的引用或者说指针被传递到了方法内部,而不是这个原始参数的内容。

2.在Java中参数是怎么传递的

在Java中,不管原始参数的类型是什么,参数都是按值传递的。每次当一个方法被执行的时候,在堆栈中就会为每个参数创建一个拷贝,这个拷贝会被传递到方法内部。

  • 如果原始参数是基本类型,那么在堆栈中创建的便是这个参数的简单拷贝
  • 如果原始参数不是基本类型,那么在堆栈中创建的便是指向真正对象数据的新的引用或指针。这个新的引用被传递到方法内部(在这种情况下,有2个引用指向了同一个对象数据)

3.解决疑惑

在接下来的示例中,我们通过往方法中传递不同类型的参数(基本类型,包装类,集合类,自定义类),在方法执行完成后去检查他们是否被修改了来尝试证明“在Java中参数传递永远是值传递”。

基本类型参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public static void main(String[] args) {
int x = 1;
int y = 2;
System.out.print("Values of x & y before primitive modification: ");
System.out.println(" x = " + x + " ; y = " + y );
modifyPrimitiveTypes(x,y);
System.out.print("Values of x & y after primitive modification: ");
System.out.println(" x = " + x + " ; y = " + y );
}
private static void modifyPrimitiveTypes(int x, int y)
{
x = 5;
y = 10;
}

输出:

1
2
复制代码Values of x & y before primitive modification:  x = 1 ; y = 2
Values of x & y after primitive modification: x = 1 ; y = 2

说明:
x,y这2个参数是基本类型,所以存储在堆栈中。当调用modifyPrimitiveTypes()方法时,在堆栈中创建了这2个参数的拷贝(我们就叫它们w,z),实际上是w,z被传递到了方法中。所以原始的参数并没有被传递到方法中,在方法中的任何修改都只作用于参数的拷贝w,z

2.png

包装类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public static void main(String[] args) {
Integer obj1 = new Integer(1);
Integer obj2 = new Integer(2);
System.out.print("Values of obj1 & obj2 before wrapper modification: ");
System.out.println("obj1 = " + obj1.intValue() + " ; obj2 = " + obj2.intValue());
modifyWrappers(obj1, obj2);
System.out.print("Values of obj1 & obj2 after wrapper modification: ");
System.out.println("obj1 = " + obj1.intValue() + " ; obj2 = " + obj2.intValue());
}
private static void modifyWrappers(Integer x, Integer y)
{
x = new Integer(5);
y = new Integer(10);
}

输出:

1
2
复制代码Values of obj1 & obj2 before wrapper modification: obj1 = 1 ; obj2 = 2
Values of obj1 & obj2 after wrapper modification: obj1 = 1 ; obj2 = 2

说明:
包装类存储在堆中,在堆栈中有一个指向它的引用
当调用modifyWrappers()方法时,在堆栈中为每个引用创建了一个拷贝,这些拷贝被传递到了方法里。任何在方法里面的修改都只是改变了引用的拷贝,而不是原始的引用

3.png

P.S: 如果方法中的表达式为x += 2,x值得改变也不会影响到方法外部,因为包装类是immutable类型的。当他们的state变化时,他们就会创建一个新的实例。如果你想了解更多关于immutable类,可以阅读How to create an immutable class in Java。字符串类型和包装类相似,所以以上的规则对于字符串也有效。

集合类型

1
2
3
4
5
6
7
8
9
10
11
复制代码public static void main(String[] args) {
List<Integer> lstNums = new ArrayList<Integer>();
lstNums.add(1);
System.out.println("Size of list before List modification = " + lstNums.size());
modifyList(lstNums);
System.out.println("Size of list after List modification = " + lstNums.size());
}
private static void modifyList(List<Integer> lstParam)
{
lstParam.add(2);
}

输出:

1
2
复制代码Size of list before List modification = 1
Size of list after List modification = 2

说明:
当我们创建一个ArrayList或任意集合,在堆栈中便会创建一个指向堆中多个对象的引用。当modifyList()被调用时,一个引用的拷贝被创建中传递到了方法中。现在有2个引用指向了真正的对象数据,其中任何一个引用的数据改变会影响到另一个。
在方法中,当我们调用lstParam.add(2)时,一个Integer对象在堆中被创建,添加到了现有的list对象。所以原始的list引用可以看见这次修改,因为2个引用都指向了内存中的同一个对象

4.png

自定义对象

1
2
3
4
5
6
7
8
9
10
复制代码public static void main(String[] args) {
Student student = new Student();
System.out.println("Value of name before Student modification = " + student.getName());
modifyStudent(student);
System.out.println("Value of name after Student modification = " + student.getName());
}
private static void modifyStudent(Student student)
{
student.setName("Alex");
}

输出:

1
2
复制代码Value of name before Student modification = null
Value of name after Student modification = Alex

说明:
student对象在堆中被创建,在堆栈中存储着指向它的引用。当调用calling modifyStudent(),在堆栈中创建了这个引用的拷贝,传递到了方法中。所以任何对这个对象属性的修改会影响原始的对象引用

结论

在Java中,参数都是按值传递的。被传递到方法中的拷贝值,要不就是一个引用或一个变量,取决于原始参数的类型。从现在开始,下面的几条规则将帮助你理解方法中对于参数的修改怎么影响原始参数变量。

  1. 在方法中,修改一个基础类型的参数永远不会影响原始参数值。

  2. 在方法中,改变一个对象参数的引用永远不会影响到原始引用。然而,它会在堆中创建了一个全新的对象。(译者注:指的是包装类和immutable对象)

  3. 在方法中,修改一个对象的属性会影响原始对象参数。

  4. 在方法中,修改集合和Maps会影响原始集合参数。

    下一篇:利用docker-compose快速部署php-fpm+nginx环境

本文转载自: 掘金

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

Predis 中的一些PHP操作redis的方法

发表于 2017-12-13

predis是php连接redis的操作库,由于它完全使用php编写,大量使用命名空间以及闭包等功能,只支持php5.3以上版本,使用方便,但是性能一般,如果追求这块的性能,可以改换c语言编写的php扩展后性能会大幅提升(比如使用C扩展phpredis github.com/owlient/php…)。

使用autoload加载相关库,这边重点就是为了require $file;

1
2
3
4
5
6
7
php复制代码spl_autoload_register ( function ( $class )  {
$file = __DIR__ . '/lib/Predis/' . $class . '.php' ;
if ( file_exists ( $file ) ) {
require $file ;
return true ;
}
} ) ;

配置连接的IP、端口、以及相应的数据库

1
2
3
4
5
6
ini复制代码$server  =  array (
'host' => '127.0.0.1' ,
'port' => 6379 ,
'database' => 15
) ;
$redis = new Client ( $server ) ;

相关操作

基础操作

普通set/get操作

redis−>set(′library′,′predis′);redis -> set ( ‘library’ , ‘predis’ ) ;
redis−>set(′library′,′predis′);retval = redis−>get(′library′);echoredis -> get ( ‘library’ ) ;
echo redis−>get(′library′);echoretval ; //显示 ‘predis’

setex set一个存储时效

$redis -> setex ( ‘str’ , 10 , ‘bar’ ) ; //表示存储有效期为10秒

setnx/msetnx相当于add操作,不会覆盖已有值

redis−>setnx(′foo′,12);//trueredis -> setnx ( ‘foo’ , 12 ) ; //true
redis−>setnx(′foo′,12);//trueredis -> setnx ( ‘foo’ , 34 ) ; //false

getset操作,set的变种,结果返回替换前的值

$redis -> getset ( ‘foo’ , 56 ) ; //返回34

incrby/incr/decrby/decr 对值的递增和递减

redis−>incr(′foo′);//foo为57redis -> incr ( ‘foo’ ) ; //foo为57
redis−>incr(′foo′);//foo为57redis -> incrby ( ‘foo’ , 2 ) ; //foo为59

exists检测是否存在某值

$redis -> exists ( ‘foo’ ) ; //true

del 删除

$redis -> del ( ‘foo’ ) ; //true

type 类型检测,字符串返回string,列表返回 list,set表返回set/zset,hash表返回hash

redis−>type(′foo′);//不存在,返回noneredis -> type ( ‘foo’ ) ; //不存在,返回none
redis−>type(′foo′);//不存在,返回noneredis -> set ( ‘str’ , ‘test’ ) ;
$redis -> type ( ‘str’ ) ; //字符串,返回string

append 连接到已存在字符串

$redis -> append ( ‘str’ , ‘_123’ ) ; //返回累加后的字符串长度8,此进str为 ‘test_123’

setrange 部分替换操作

redis−>setrange(′str′,0,′abc′);//返回3,参数2为0时等同于set操作redis -> setrange ( ‘str’ , 0 , ‘abc’ ) ; //返回3,参数2为0时等同于set操作
redis−>setrange(′str′,0,′abc′);//返回3,参数2为0时等同于set操作redis -> setrange ( ‘str’ , 2 , ‘cd’ ) ; //返回4,表示从第2个字符后替换,这时’str’为’abcd’

substr 部分获取操作

$redis -> substr ( ‘str’ , 0 , 2 ) ; //表示从第0个起,取到第2个字符,共3个,返回’abc’

strlen 获取字符串长度

$redis -> strlen ( ‘str’ ) ; //返回4

setbit/getbit 位存储和获取

redis−>setbit(′binary′,31,1);//表示在第31位存入1,这边可能会有大小端问题?不过没关系,getbit应该不会有问题redis -> setbit ( ‘binary’ , 31 , 1 ) ; //表示在第31位存入1,这边可能会有大小端问题?不过没关系,getbit 应该不会有问题
redis−>setbit(′binary′,31,1);//表示在第31位存入1,这边可能会有大小端问题?不过没关系,getbit应该不会有问题redis -> getbit ( ‘binary’ , 31 ) ; //返回1

keys 模糊查找功能,支持*号以及?号(匹配一个字符)

redis−>set(′foo1′,123);redis -> set ( ‘foo1’ , 123 ) ;
redis−>set(′foo1′,123);redis -> set ( ‘foo2’ , 456 ) ;
redis−>keys(′foo∗′);//返回foo1和foo2的arrayredis -> keys ( ‘foo*‘ ) ; //返回foo1和foo2的array
redis−>keys(′foo∗′);//返回foo1和foo2的arrayredis -> keys ( ‘f?o?’ ) ; //同上

randomkey 随机返回一个key

$redis -> randomkey ( ) ; //可能是返回 ‘foo1’或者是’foo2’及其它任何一存在redis的key

rename/renamenx 对key进行改名,所不同的是renamenx不允许改成已存在的key

$redis -> rename ( ‘str’ , ‘str2’ ) ; //把原先命名为’str’的key改成了’str2’

######expire 设置key-value的时效性,ttl 获取剩余有效期,persist 重新设置为永久存储
redis−>expire(′foo′,1);//设置有效期为1秒redis -> expire ( ‘foo’ , 1 ) ; //设置有效期为1秒
redis−>expire(′foo′,1);//设置有效期为1秒redis -> ttl ( ‘foo’ ) ; //返回有效期值1s
$redis -> expire ( ‘foo’ ) ; //取消expire行为

######dbsize 返回redis当前数据库的记录总数
$redis -> dbsize ( ) ;

####队列操作

rpush/rpushx 有序列表操作,从队列后插入元素,lpush/lpushx 和rpush/rpushx的区别是插入到队列的头部,同上,’x’含义是只对已存在的key进行操作

redis−>rpush(′fooList′,′bar1′);//返回一个列表的长度1redis -> rpush ( ‘fooList’ , ‘bar1’ ) ; //返回一个列表的长度1
redis−>rpush(′fooList′,′bar1′);//返回一个列表的长度1redis -> lpush ( ‘fooList’ , ‘bar0’ ) ; //返回一个列表的长度2
$redis -> rpushx ( ‘fooList’ , ‘bar2’ ) ; //返回3,rpushx只对已存在的队列做添加,否则返回0

llen返回当前列表长度

$redis -> llen ( ‘fooList’ ) ; //3

######lrange 返回队列中一个区间的元素
redis−>lrange(′fooList′,0,1);//返回数组包含第0个至第1个共2个元素redis -> lrange ( ‘fooList’ , 0 , 1 ) ; //返回数组包含第0个至第1个共2个元素
redis−>lrange(′fooList′,0,1);//返回数组包含第0个至第1个共2个元素redis -> lrange ( ‘fooList’ , 0 ,- 1 ) ; //返回第0个至倒数第一个,相当于返回所有元素,注意redis中很多时候会用到负数,下同

lindex 返回指定顺序位置的list元素

$redis -> lindex ( ‘fooList’ , 1 ) ; //返回’bar1’

lset 修改队列中指定位置的value

$redis -> lset ( ‘fooList’ , 1 , ‘123’ ) ; //修改位置1的元素,返回true

lrem 删除队列中左起指定数量的字符

$redis -> lrem ( ‘fooList’ , 1 , ‘‘ ) ; //删除队列中左起(右起使用-1)1个字符’‘(若有)

lpop/rpop 类似栈结构地弹出(并删除)最左或最右的一个元素

redis−>lpop(′fooList′);//′bar0′redis -> lpop ( ‘fooList’ ) ; //‘bar0’
redis−>lpop(′fooList′);//′bar0′redis -> rpop ( ‘fooList’ ) ; //‘bar2’

ltrim 队列修改,保留左边起若干元素,其余删除

$redis -> ltrim ( ‘fooList’ , 0 , 1 ) ; //保留左边起第0个至第1个元素

rpoplpush 从一个队列中pop出元素并push到另一个队列

redis−>rpush(′list1′,′ab0′);redis -> rpush ( ‘list1’ , ‘ab0’ ) ;
redis−>rpush(′list1′,′ab0′);redis -> rpush ( ‘list1’ , ‘ab1’ ) ;
redis−>rpush(′list2′,′ab2′);redis -> rpush ( ‘list2’ , ‘ab2’ ) ;
redis−>rpush(′list2′,′ab2′);redis -> rpush ( ‘list2’ , ‘ab3’ ) ;
redis−>rpoplpush(′list1′,′list2′);//结果list1=>array(′ab0′),list2=>array(′ab1′,′ab2′,′ab3′)redis -> rpoplpush ( ‘list1’ , ‘list2’ ) ; //结果list1 =>array(‘ab0’),list2 =>array(‘ab1’,’ab2’,’ab3’)
redis−>rpoplpush(′list1′,′list2′);//结果list1=>array(′ab0′),list2=>array(′ab1′,′ab2′,′ab3′)redis -> rpoplpush ( ‘list2’ , ‘list2’ ) ; //也适用于同一个队列,把最后一个元素移到头部list2 =>array(‘ab3’,’ab1’,’ab2’)

linsert 在队列的中间指定元素前或后插入元素

redis−>linsert(′list2′,′before′,′ab1′,′123′);//表示在元素′ab1′之前插入′123′redis -> linsert ( ‘list2’ , ‘before’ , ‘ab1’ , ‘123’ ) ; //表示在元素’ab1’之前插入’123’
redis−>linsert(′list2′,′before′,′ab1′,′123′);//表示在元素′ab1′之前插入′123′redis -> linsert ( ‘list2’ , ‘after’ , ‘ab1’ , ‘456’ ) ; //表示在元素’ab1’之后插入’456’

blpop/brpop 阻塞并等待一个列队不为空时,再pop出最左或最右的一个元素(这个功能在php以外可以说非常好用),brpoplpush 同样是阻塞并等待操作,结果同rpoplpush一样

$redis -> blpop ( ‘list3’ , 10 ) ;

如果list3为空则一直等待,直到不为空时将第一元素弹出,10秒后超时

set表操作

sadd 增加元素,返回true,重复返回false

redis−>sadd(′set1′,′ab′);redis -> sadd ( ‘set1’ , ‘ab’ ) ;
redis−>sadd(′set1′,′ab′);redis -> sadd ( ‘set1’ , ‘cd’ ) ;
$redis -> sadd ( ‘set1’ , ‘ef’ ) ;

srem 移除指定元素

$redis -> srem ( ‘set1’ , ‘cd’ ) ; //删除’cd’元素

spop 移除并返回集合中的一个随机元素

$redis -> spop ( ‘set1’ ) ;

smove 移动当前set表的指定元素到另一个set表

redis−>sadd(′set2′,′123′);redis -> sadd ( ‘set2’ , ‘123’ ) ;
redis−>sadd(′set2′,′123′);redis -> smove ( ‘set1’ , ‘set2’ , ‘ab’ ) ; //移动’set1’中的’ab’到’set2’,返回true or false

scard 返回当前set表元素个数

$redis -> scard ( ‘set2’ ) ; //2

######sismember 判断元素是否属于当前表
$redis -> sismember ( ‘set2’ , ‘123’ ) ; //true or false

smembers 返回当前表的所有元素

$redis -> smembers ( ‘set2’ ) ; //array(‘123’,’ab’);

sinter/sunion/sdiff 返回两个表中元素的交集/并集/补集

redis−>sadd(′set1′,′ab′);redis -> sadd ( ‘set1’ , ‘ab’ ) ;
redis−>sadd(′set1′,′ab′);redis -> sinter ( ‘set2’ , ‘set1’ ) ; //返回array(‘ab’)

sinterstore/sunionstore/sdiffstore 将两个表交集/并集/补集元素copy到第三个表中
1
2
3
bash复制代码$redis -> set ( 'foo' , 0 ) ;
$redis -> sinterstore ( 'foo' , 'set1' ) ; //这边等同于将'set1'的内容copy到'foo'中,并将'foo'转为set表
$redis -> sinterstore ( 'foo' , array ( 'set1' , 'set2' ) ) ; //将'set1'和'set2'中相同的元素copy到'foo'表中,覆盖'foo'原有内容
srandmember 返回表中一个随机元素

$redis -> srandmember ( ‘set1’ ) ;

有序set表操作

增加元素,并设置序号,返回true,重复返回false

redis−>zadd(′zset1′,1,′ab′);redis -> zadd ( ‘zset1’ , 1 , ‘ab’ ) ;
redis−>zadd(′zset1′,1,′ab′);redis -> zadd ( ‘zset1’ , 2 , ‘cd’ ) ;
$redis -> zadd ( ‘zset1’ , 3 , ‘ef’ ) ;

zincrby 对指定元素索引值的增减,改变元素排列次序

$redis -> zincrby ( ‘zset1’ , 10 , ‘ab’ ) ; //返回11

zrem 移除指定元素

$redis -> zrem ( ‘zset1’ , ‘ef’ ) ; //true or false

zrange 按位置次序返回表中指定区间的元素

redis−>zrange(′zset1′,0,1);//返回位置0和1之间(两个)的元素redis -> zrange ( ‘zset1’ , 0 , 1 ) ; //返回位置0和1之间(两个)的元素
redis−>zrange(′zset1′,0,1);//返回位置0和1之间(两个)的元素redis -> zrange ( ‘zset1’ , 0 ,- 1 ) ; //返回位置0和倒数第一个元素之间的元素(相当于所有元素)

zrevrange 同上,返回表中指定区间的元素,按次序倒排

$redis -> zrevrange ( ‘zset1’ , 0 ,- 1 ) ; //元素顺序和zrange相反

zrangebyscore/zrevrangebyscore 按顺序/降序返回表中指定索引区间的元素
1
2
3
4
5
6
sql复制代码$redis -> zadd ( 'zset1' , 3 , 'ef' ) ;
$redis -> zadd ( 'zset1' , 5 , 'gh' ) ;
$redis -> zrangebyscore ( 'zset1' , 2 , 9 ) ; //返回索引值2-9之间的元素 array('ef','gh')
//参数形式
$redis -> zrangebyscore ( 'zset1' , 2 , 9 , 'withscores' ) ; //返回索引值2-9之间的元素并包含索引值 array(array('ef',3),array('gh',5))
$redis -> zrangebyscore ( 'zset1' , 2 , 9 , array ( 'withscores' => true , 'limit' => array ( 1 , 2 ) ) ) ; //返回索引值2-9之间的元素,'withscores' =>true表示包含索引值; 'limit'=>array(1, 2),表示最多返回2条,结果为array(array('ef',3),array('gh',5))
zunionstore/zinterstore 将多个表的并集/交集存入另一个表中
1
2
3
4
perl复制代码$redis -> zunionstore ( 'zset3' , array ( 'zset1' , 'zset2' , 'zset0' ) ) ;  //将'zset1','zset2','zset0'的并集存入'zset3'
//其它参数
$redis -> zunionstore ( 'zset3' , array ( 'zset1' , 'zset2' ) , array ( 'weights' => array ( 5 , 0 ) ) ) ; //weights参数表示权重,其中表示并集后值大于5的元素排在前,大于0的排在后
$redis -> zunionstore ( 'zset3' , array ( 'zset1' , 'zset2' ) , array ( 'aggregate' => 'max' ) ) ; //'aggregate' => 'max'或'min'表示并集后相同的元素是取大值或是取小值
zcount 统计一个索引区间的元素个数

redis−>zcount(′zset1′,3,5);//2redis -> zcount ( ‘zset1’ , 3 , 5 ) ; //2
redis−>zcount(′zset1′,3,5);//2redis -> zcount ( ‘zset1’ , ‘(3’ , 5 ) ) ; //‘(3’表示索引值在3-5之间但不含3,同理也可以使用’(5’表示上限为5但不含5

zcard 统计元素个数

$redis -> zcard ( ‘zset1’ ) ; //4

zscore 查询元素的索引

$redis -> zscore ( ‘zset1’ , ‘ef’ ) ; //3

zremrangebyscore 删除一个索引区间的元素

$redis -> zremrangebyscore ( ‘zset1’ , 0 , 2 ) ; //删除索引在0-2之间的元素(‘ab’,’cd’),返回删除元素个数2

zrank/zrevrank 返回元素所在表顺序/降序的位置(不是索引)

$redis -> zrank ( ‘zset1’ , ‘ef’ ) ; //返回0,因为它是第一个元素;zrevrank则返回1(最后一个)

zremrangebyrank 删除表中指定位置区间的元素

$redis -> zremrangebyrank ( ‘zset1’ , 0 , 10 ) ; //删除位置为0-10的元素,返回删除的元素个数2

hash表操作

hset/hget 存取hash表的数据

redis−>hset(′hash1′,′key1′,′v1′);//将key为′key1′value为′v1′的元素存入hash1表redis -> hset ( ‘hash1’ , ‘key1’ , ‘v1’ ) ; //将key为’key1’ value为’v1’的元素存入hash1表
redis−>hset(′hash1′,′key1′,′v1′);//将key为′key1′value为′v1′的元素存入hash1表redis -> hset ( ‘hash1’ , ‘key2’ , ‘v2’ ) ;
$redis -> hget ( ‘hash1’ , ‘key1’ ) ; //取出表’hash1’中的key ‘key1’的值,返回’v1’

hexists 返回hash表中的指定key是否存在

$redis -> hexists ( ‘hash1’ , ‘key1’ ) ; //true or false

hdel 删除hash表中指定key的元素

$redis -> hdel ( ‘hash1’ , ‘key2’ ) ; //true or false

hlen 返回hash表元素个数

$redis -> hlen ( ‘hash1’ ) ; //1

hsetnx 增加一个元素,但不能重复

redis−>hsetnx(′hash1′,′key1′,′v2′);//falseredis -> hsetnx ( ‘hash1’ , ‘key1’ , ‘v2’ ) ; //false
redis−>hsetnx(′hash1′,′key1′,′v2′);//falseredis -> hsetnx ( ‘hash1’ , ‘key2’ , ‘v2’ ) ; //true

hmset/hmget 存取多个元素到hash表

redis−>hmset(′hash1′,array(′key3′=>′v3′,′key4′=>′v4′));redis -> hmset ( ‘hash1’ , array ( ‘key3’ => ‘v3’ , ‘key4’ => ‘v4’ ) ) ;
redis−>hmset(′hash1′,array(′key3′=>′v3′,′key4′=>′v4′));redis -> hmget ( ‘hash1’ , array ( ‘key3’ , ‘key4’ ) ) ; //返回相应的值 array(‘v3’,’v4’)

hincrby 对指定key进行累加

redis−>hincrby(′hash1′,′key5′,3);//返回3redis -> hincrby ( ‘hash1’ , ‘key5’ , 3 ) ; //返回3
redis−>hincrby(′hash1′,′key5′,3);//返回3redis -> hincrby ( ‘hash1’ , ‘key5’ , 10 ) ; //返回13

hkeys 返回hash表中的所有key

$redis -> hkeys ( ‘hash1’ ) ; //返回array(‘key1’,’key2’,’key3’,’key4’,’key5’)

//hvals 返回hash表中的所有value
$redis -> hvals ( ‘hash1’ ) ; //返回array(‘v1’,’v2’,’v3’,’v4’,13)

hgetall 返回整个hash表元素

$redis -> hgetall ( ‘hash1’ ) ; //返回array(‘key1’=>’v1’,’key2’=>’v2’,’key3’=>’v3’,’key4’=>’v4’,’key5’=>13)

排序操作

sort 排序
1
2
3
4
5
6
7
8
9
10
perl复制代码$redis -> rpush ( 'tab' , 3 ) ;
$redis -> rpush ( 'tab' , 2 ) ;
$redis -> rpush ( 'tab' , 17 ) ;
$redis -> sort ( 'tab' ) ; //返回array(2,3,17)
//使用参数,可组合使用 array('sort' => 'desc','limit' => array(1, 2))
$redis -> sort ( 'tab' , array ( 'sort' => 'desc' ) ) ; //降序排列,返回array(17,3,2)
$redis -> sort ( 'tab' , array ( 'limit' => array ( 1 , 2 ) ) ) ; //返回顺序位置中1的元素2个(这里的2是指个数,而不是位置),返回array(3,17)
$redis -> sort ( 'tab' , array ( 'limit' => array ( 'alpha' => true ) ) ) ; //按首字符排序返回array(17,2,3),因为17的首字符是'1'所以排首位置
$redis -> sort ( 'tab' , array ( 'limit' => array ( 'store' => 'ordered' ) ) ) ; //表示永久性排序,返回元素个数
$redis -> sort ( 'tab' , array ( 'limit' => array ( 'get' => 'pre_*' ) ) ) ; //使用了通配符'*'过滤元素,表示只返回以'pre_'开头的元素

redis管理操作

select 指定要操作的数据库

$redis -> select ( ‘mydb’ ) ; //指定为mydb,不存在则创建

flushdb 清空当前库

$redis -> flushdb ( ) ;

move 移动当库的元素到其它库

redis−>set(′foo′,′bar′);redis -> set ( ‘foo’ , ‘bar’ ) ;
redis−>set(′foo′,′bar′);redis -> move ( ‘foo’ , ‘mydb2’ ) ; //若’mydb2’库存在

info 显示服务当状态信息

$redis -> info ( ) ;

slaveof 配置从服务器

redis−>slaveof(′127.0.0.1′,80);//配置127.0.0.1端口80的服务器为从服务器redis -> slaveof ( ‘127.0.0.1’ , 80 ) ; //配置127.0.0.1端口80的服务器为从服务器
redis−>slaveof(′127.0.0.1′,80);//配置127.0.0.1端口80的服务器为从服务器redis -> slaveof ( ) ; //清除从服务器

同步保存服务器数据到磁盘

$redis -> save ( ) ;

异步保存服务器数据到磁盘

$redis -> bgsave ( ) ;

保存数据到磁盘

$redis -> bgrewriteaof ( ) ;

返回最后更新磁盘的时间

$redis -> lastsave ( ) ;

set/get多个key-value

1
2
3
4
5
6
7
8
ini复制代码$mkv  =  array (
'usr:0001' => 'First user' ,
'usr:0002' => 'Second user' ,
'usr:0003' => 'Third user'
) ;
$redis -> mset ( $mkv ) ; //存储多个key对应的value
$retval = $redis -> mget ( array_keys ( $mkv ) ) ; //获取多个key对应的value
print_r ( $retval ) ;
批量操作
1
2
3
4
5
6
7
8
9
10
perl复制代码$replies  =  $redis -> pipeline ( function ( $pipe )  {
$pipe -> ping ( ) ;
$pipe -> flushdb ( ) ;
$pipe -> incrby ( 'counter' , 10 ) ; //增量操作
$pipe -> incrby ( 'counter' , 30 ) ;
$pipe -> exists ( 'counter' ) ;
$pipe -> get ( 'counter' ) ;
$pipe -> mget ( 'does_not_exist' , 'counter' ) ;
} ) ;
print_r ( $replies ) ;

CAS,事务性操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
php复制代码function zpop ( $client ,  $zsetKey )  {
$element = null ;
$options = array (
'cas' => true , // Initialize with support for CAS operations
'watch' => $zsetKey , // Key that needs to be WATCHed to detect changes
'retry' => 3 , // Number of retries on aborted transactions, after
// which the client bails out with an exception.
) ;

$txReply = $client -> multiExec ( $options , function ( $tx )
use ( $zsetKey , & $element ) {
@ list ( $element ) = $tx -> zrange ( $zsetKey , 0 , 0 ) ;
if ( isset ( $element ) ) {
$tx -> multi ( ) ; // With CAS, MULTI *must* be explicitly invoked.
$tx -> zrem ( $zsetKey , $element ) ;
}
} ) ;
return $element ;
}
$zpopped = zpop ( $redis , 'zset' ) ;
echo isset ( $zpopped ) ? "ZPOPed $zpopped" : "Nothing to ZPOP!" , "\n" ;

对存取的key加前缀,如: ‘nrk:’

1
php复制代码$redis -> getProfile ( ) -> setPreprocessor ( new KeyPrefixPreprocessor ( 'nrk:' ) ) ;

分布式存储的一些方法

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
php复制代码$multiple_servers  =  array (
array (
'host' => '127.0.0.1' ,
'port' => 6379 ,
'database' => 15 ,
'alias' => 'first' ,
) ,
array (
'host' => '127.0.0.1' ,
'port' => 6380 ,
'database' => 15 ,
'alias' => 'second' ,
) ,
) ;

use Predis\Distribution\IDistributionStrategy ;

class NaiveDistributionStrategy implements IDistributionStrategy {
private $_nodes , $_nodesCount ;

public function __constructor ( ) {
$this ->_nodes = array ( ) ;
$this ->_nodesCount = 0 ;
}

public function add ( $node , $weight = null ) {
$this ->_nodes [ ] = $node ;
$this ->_nodesCount ++;
}

public function remove ( $node ) {
$this ->_nodes = array_filter ( $this ->_nodes , function ( $n ) use ( $node ) {
return $n !== $node ;
} ) ;
$this ->_nodesCount = count ( $this ->_nodes ) ;
}

public function get ( $key ) {
$count = $this ->_nodesCount ;
if ( $count === 0 ) {
throw new RuntimeException ( 'No connections' ) ;
}
return $this ->_nodes [ $count > 1 ? abs ( crc32 ( $key ) % $count ) : 0 ] ;
}

public function generateKey ( $value ) {
return crc32 ( $value ) ;
}
}

配置键分布策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
perl复制代码$options  =  array (
'key_distribution' => new NaiveDistributionStrategy ( ) ,
) ;

$redis = new Predis\Client ( $multiple_servers , $options ) ;

for ( $i = 0 ; $i set ( "key:$i" , str_pad ( $i , 4 , '0' , 0 ) ) ;
$redis -> get ( "key:$i" ) ;
}

$server1 = $redis -> getClientFor ( 'first' ) -> info ( ) ;
$server2 = $redis -> getClientFor ( 'second' ) -> info ( ) ;

printf ( "Server '%s' has %d keys while server '%s' has %d keys.\n" ,
'first' , $server1 [ 'db15' ] [ 'keys' ] , 'second' , $server2 [ 'db15' ] [ 'keys' ]
) ;

本文转载自: 掘金

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

golang 如何验证struct字段的数据格式

发表于 2017-12-13

本文同时发表在github.com/zhangyachen…

假设我们有如下结构体:

1
2
3
4
5
6
复制代码type User struct {
Id int
Name string
Bio string
Email string
}

我们需要对结构体内的字段进行验证合法性:

  • Id的值在某一个范围内。
  • Name的长度在某一个范围内。
  • Email格式正确。

我们可能会这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码user := User{
Id: 0,
Name: "superlongstring",
Bio: "",
Email: "foobar",
}

if user.Id < 1 && user.Id > 1000 {
return false
}
if len(user.Name) < 2 && len(user.Name) > 10 {
return false
}
if !validateEmail(user.Email) {
return false
}

这样的话代码比较冗余,而且如果结构体新加字段,还需要再修改验证函数再加一段if判断。这样代码比较冗余。我们可以借助golang的structTag来解决上述的问题:

1
2
3
4
5
6
复制代码type User struct {
Id int `validate:"number,min=1,max=1000"`
Name string `validate:"string,min=2,max=10"`
Bio string `validate:"string"`
Email string `validate:"email"`
}

validate:"number,min=1,max=1000"就是structTag。如果对这个比较陌生的话,看看下面这个:

1
2
3
4
5
6
7
8
9
复制代码
type User struct {
Id int `json:"id"`
Name string `json:"name"`
Bio string `json:"about,omitempty"`
Active bool `json:"active"`
Admin bool `json:"-"`
CreatedAt time.Time `json:"created_at"`
}

写过golang的基本都用过json:xxx这个用法,json:xxx其实也是一个structTag,只不过这是golang帮你实现好特定用法的structTag。而validate:"number,min=1,max=1000"是我们自定义的structTag。

实现思路

image

我们定义一个接口Validator,定义一个方法Validate。再定义有具体意义的验证器例如StringValidator、NumberValidator、EmailValidator来实现接口Validator。
这里为什么要使用接口?假设我们不使用接口代码会怎么写?

1
2
3
4
5
6
7
8
9
复制代码if tagIsOfNumber(){
validator := NumberValidator{}
}else if tagIsOfString() {
validator := StringValidator{}
}else if tagIsOfEmail() {
validator := EmailValidator{}
}else if tagIsOfDefault() {
validator := DefaultValidator{}
}

这样的话判断逻辑不能写在一个函数中,因为返回值validator会因为structTag的不同而不同,而且validator也不能当做函数参数做传递。而我们定义一个接口,所有的validator都去实现这个接口,上述的问题就能解决,而且逻辑更加清晰和紧凑。
关于接口的使用可以看下标准库的io Writer,Writer是个interface,只有一个方法Writer:

1
2
3
复制代码type Writer interface {
Write(p []byte) (n int, err error)
}

而输出函数可以直接调用参数的Write方法即可,无需关心到底是写到文件还是写到标准输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
p := newPrinter()
p.doPrintf(format, a)
n, err = w.Write(p.buf) //调用Write方法
p.free()
return
}

//调用
Fprintf(os.Stdout, format, a...) //标准输出
Fprintf(os.Stderr, msg+"\n", args...) //标准错误输出

var buf bytes.Buffer
Fprintf(&buf, "[") //写入到Buffer的缓存中

言归正传,我们看下完整代码,代码是Custom struct field tags in Golang中给出的:

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
132
133
134
135
136
137
138
139
140
141
142
143
复制代码package main

import (
"fmt"
"reflect"
"regexp"
"strings"
)

const tagName = "validate"

//邮箱验证正则
var mailRe = regexp.MustCompile(`\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z`)

//验证接口
type Validator interface {
Validate(interface{}) (bool, error)
}

type DefaultValidator struct {
}

func (v DefaultValidator) Validate(val interface{}) (bool, error) {
return true, nil
}

type StringValidator struct {
Min int
Max int
}

func (v StringValidator) Validate(val interface{}) (bool, error) {
l := len(val.(string))

if l == 0 {
return false, fmt.Errorf("cannot be blank")
}

if l < v.Min {
return false, fmt.Errorf("should be at least %v chars long", v.Min)
}

if v.Max >= v.Min && l > v.Max {
return false, fmt.Errorf("should be less than %v chars long", v.Max)
}

return true, nil
}


type NumberValidator struct {
Min int
Max int
}

func (v NumberValidator) Validate(val interface{}) (bool, error) {
num := val.(int)

if num < v.Min {
return false, fmt.Errorf("should be greater than %v", v.Min)
}

if v.Max >= v.Min && num > v.Max {
return false, fmt.Errorf("should be less than %v", v.Max)
}

return true, nil
}

type EmailValidator struct {
}

func (v EmailValidator) Validate(val interface{}) (bool, error) {
if !mailRe.MatchString(val.(string)) {
return false, fmt.Errorf("is not a valid email address")
}
return true, nil
}

func getValidatorFromTag(tag string) Validator {
args := strings.Split(tag, ",")

switch args[0] {
case "number":
validator := NumberValidator{}
//将structTag中的min和max解析到结构体中
fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &validator.Min, &validator.Max)
return validator
case "string":
validator := StringValidator{}
fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &validator.Min, &validator.Max)
return validator
case "email":
return EmailValidator{}
}

return DefaultValidator{}
}

func validateStruct(s interface{}) []error {
errs := []error{}

v := reflect.ValueOf(s)

for i := 0; i < v.NumField(); i++ {
//利用反射获取structTag
tag := v.Type().Field(i).Tag.Get(tagName)

if tag == "" || tag == "-" {
continue
}

validator := getValidatorFromTag(tag)

valid, err := validator.Validate(v.Field(i).Interface())
if !valid && err != nil {
errs = append(errs, fmt.Errorf("%s %s", v.Type().Field(i).Name, err.Error()))
}
}

return errs
}

type User struct {
Id int `validate:"number,min=1,max=1000"`
Name string `validate:"string,min=2,max=10"`
Bio string `validate:"string"`
Email string `validate:"email"`
}

func main() {
user := User{
Id: 0,
Name: "superlongstring",
Bio: "",
Email: "foobar",
}

fmt.Println("Errors:")
for i, err := range validateStruct(user) {
fmt.Printf("\t%d. %s\n", i+1, err.Error())
}
}

代码很好理解,结构也很清晰,不做过多解释了^_^

github上其实已经有现成的验证包了govalidator,支持内置支持的验证tag和自定义验证tag:

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
复制代码package main

import (
"github.com/asaskevich/govalidator"
"fmt"
"strings"
)

type Server struct {
ID string `valid:"uuid,required"`
Name string `valid:"machine_id"`
HostIP string `valid:"ip"`
MacAddress string `valid:"mac,required"`
WebAddress string `valid:"url"`
AdminEmail string `valid:"email"`
}

func main() {
server := &Server{
ID: "123e4567-e89b-12d3-a456-426655440000",
Name: "IX01",
HostIP: "127.0.0.1",
MacAddress: "01:23:45:67:89:ab",
WebAddress: "www.example.com",
AdminEmail: "admin@exmaple.com",
}

//自定义tag验证函数
govalidator.TagMap["machine_id"] = govalidator.Validator(func(str string) bool {
return strings.HasPrefix(str, "IX")
})

if ok, err := govalidator.ValidateStruct(server); err != nil {
panic(err)
} else {
fmt.Printf("OK: %v\n", ok)
}
}

参考资料:

  • Custom struct field tags in Golang
  • Data validation in Golang
  • govalidator

本文转载自: 掘金

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

和我一起读Java8 LinkedList源码

发表于 2017-12-13

书接上一篇ArrayList源码解析,这一节继续分析LinkedList在Java8中的实现,它同样实现了List接口,不过由名字就可以知道,内部实现是基于链表的,而且是双向链表,所以Linked List在执行像插入或者删除这样的操作,效率是极高的,相对地,在随机访问方面就弱了很多。
本文基于JDK1.8中LinkedList源码分析
####类定义

LinkedList继承

1
2
3
复制代码public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable

由上图以及类定义片段可知,LinkedList继承了AbstractSequentialList并且实现List,Deque,Cloneable, Serializable接口。
其中,AbstractSequentialList相较于AbstractList(ArrayList的父类),只支持次序访问,而不支持随机访问,因为它的
get(int index) ,set(int index, E element), add(int index, E element), remove(int index) 都是基于迭代器实现的。所以在LinkedList使用迭代器遍历更快,而ArrayList使用get (i)更快。
接口方面,LinkedList多继承了一个Deque接口,所以实现了双端队列的一系列方法。

####基本数据结构

1
2
3
复制代码transient int size = 0;
transient Node<E> first;
transient Node<E> last;

LinkedList中主要定义了头节点指针,尾节点指针,以及size用于计数链表中节点个数。那么每一个Node的结构如何呢?

1
2
3
4
5
6
7
8
9
10
11
复制代码private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;

Node(Node<E> prev, E element, Node<E> next) {
this.item = element; //当前节点值
this.next = next; //后继节点
this.prev = prev;//前驱节点
}
}

可以看出,这是一个典型的双向链表的节点。

Node节点

LinkedList先不从初始化聊起,首先谈插入与删除。
####获取节点
获取节点是相对比较简单的操作, LinkedList提供了:

  • getFirst获取头节点
  • getLast获取尾节点
  • get(int index) 获取指定位置的节点
1
2
3
4
5
6
复制代码public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}

检查非空后,直接返回first节点的item

1
2
3
4
5
6
复制代码public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}

检查非空后,直接返回last节点的item

1
2
3
4
复制代码public E get(int index) {
checkElementIndex(index);
return node(index).item;
}

首先检查index范围,然后调用node(index)获取index处的节点,返回该节点的item值。
看看node(index)的实现,后面很多地方借助于这个小函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码Node<E> node(int index) {
// assert isElementIndex(index);

if (index < (size >> 1)) { //判断index是在链表偏左侧还是偏右侧
Node<E> x = first;
for (int i = 0; i < index; i++) //从左边往右next
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--) //从右往左prev
x = x.prev;
return x;
}
}

由上面可以看出,在链表中找一个位置,只能通过不断遍历。

另外还有IndexOf,LastIndexOf操作,找出指定元素在LinkedList中的位置:
也是一个从前找,一个从后找,只分析下IndexOf操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}

主要也是不断遍历,找到值相等的节点,返回它的Index。

####更改节点的值
主要也就是一个set函数

1
2
3
4
5
6
7
复制代码   public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}

根据index找到指定节点,更改它的值,并且会返回原有值。

####插入节点
LinkedList实现了Deque接口,支持在链表头部和尾部插入元素:

1
2
3
4
5
6
7
复制代码public void addFirst(E e) {
linkFirst(e);
}

public void addLast(E e) {
linkLast(e);
}

这里我们可以看到内部实现的函数是linkFirst,linkLast,
链表头插入元素:

1
2
3
4
5
6
7
8
9
10
11
复制代码private void linkFirst(E e) {
final Node<E> f = first; //现将原有的first节点保存在f中
final Node<E> newNode = new Node<>(null, e, f); //将新增节点包装成一个Node节点,同时该节点的next指向之前的first
first = newNode; //将新增的节点设成first
if (f == null)
last = newNode; //如果原来的first就是空,那么新增的newNode同时也是last节点
else
f.prev = newNode; //如果不是空,则原来的first节点的前置节点就是现在新增的节点
size++; //插入元素后,节点个数加1
modCount++; //还记得上一篇讲述的快速失败吗?这边依然是这个作用
}

在头部插入

主要流程:

  1. 将原有头节点保存到f
  2. 将插入元素包装成新节点,并且该新节点的next指向原来的头节点,即f
  3. 如果原来的头节点f为空的话,那么新插的头节点也是last节点,否则,还要设置f的前置节点为NewNode,即NewNode现在是first节点了
  4. 记得增加size,记录修改次数modCount

链表尾插入元素

1
2
3
4
5
6
7
8
9
10
11
复制代码void linkLast(E e) {
final Node<E> l = last; //将尾节点保存到l中
final Node<E> newNode = new Node<>(l, e, null); //把e包装成Node节点,同时把该节点的前置节点设置为l
last = newNode; //把新插的节点设置为last节点
if (l == null)
first = newNode; //如果原来的last节点为空,那么新增的节点在意义上也是first节点
else
l.next = newNode; //否则的话还要将newNode设为原有last节点的后继节点,所以newNode现在是新的Last节点
size++;
modCount++;
}

在尾部插入,详细流程见注释

在指定index处插入元素

1
2
3
4
5
6
7
8
复制代码public void add(int index, E element) {
checkPositionIndex(index);

if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}

首先调用checkPositionIndex检查index值是否在范围内,如果index在最后的话,就调用在尾部插入的函数,否则调用LinkBefore,主要看LinkBefore如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码void linkBefore(E e, Node<E> succ) {  //在succ节点前插入newNode
// assert succ != null;
final Node<E> pred = succ.prev; //将succ的前置节点记为pred
final Node<E> newNode = new Node<>(pred, e, succ); 以pred为前置节点,以succ为后继节点建立newNode
succ.prev = newNode; //将new Node设为succ的前置节点
if (pred == null)
first = newNode; //如果原有的succ的前置节点为空,那么新插入的newNode就是first节点
else
pred.next = newNode; // 否则,要把newNode设为原来pred节点的后置节点
size++;
modCount++;
}

在指定index处插入元素,流程看代码注释

其余几个常用的add方法也是基于以上函数:

1
2
3
4
复制代码public boolean add(E e) {
linkLast(e);
return true;
}

add函数默认在函数尾部插入元素

1
2
3
复制代码public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}

addAll(Collection<? extends E> c)指的是在list尾部插入一个集合,具体实现又依赖于addAll(size,c),指的是在指定位置插入一个集合:

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
复制代码public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index); // 检查index位置是否超出范围

Object[] a = c.toArray(); //将集合c转变成Object数组 同时计算数组长度
int numNew = a.length;
if (numNew == 0)
return false;

Node<E> pred, succ;
if (index == size) { //如果插入位置为尾部,succ则为null,原来链表的last设置为此刻的pred节点
succ = null;
pred = last;
} else {
succ = node(index); //否则,index所在节点设置为succ,succ的前置节点设为pred
pred = succ.prev;
}

for (Object o : a) { //循环遍历数组a
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null); //以a为element元素构造Node节点
if (pred == null)
first = newNode; //如果pred为空,此Node就为first节点
else
pred.next = newNode; //否则就往pred后插入该Node
pred = newNode; //newNode此刻成为新的pred, 这样不断循环遍历,把这个数组插入到链表中
}

if (succ == null) { //如果succ为空,就把插入的最后一个节点设为last
last = pred;
} else {
pred.next = succ; //否则,把之前保存的succ接到pred后面
succ.prev = pred; //并且把succ的前向指针指向插入的最后一个元素
}

size += numNew; //记录增长的尺寸
modCount++; //记录修改次数
return true;
}

具体流程可以看代码中的注释。

####删除节点####
因为实现了Deque的接口,所以还是实现了removeFirst, removeLast方法。

1
2
3
4
5
6
复制代码public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}

首先保存fisrt节点到f,如果为空,抛出NoSuchElementException异常,实际还是调用unlinkFirst完成操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item; //把f.item的值保存到element
final Node<E> next = f.next; //把f.next的值记住
f.item = null;
f.next = null; // help GC //把item和next的都指向null
first = next; //next成为实际的first节点
if (next == null) //next为空的话,因为next是第一个节点,所以链表都是空的,last也为空
last = null;
else
next.prev = null; //next不为空,也要将它的前驱节点记为null,因为next是第一个节点
size--; //节点减少一个
modCount++; //操作次数加1
return element;
}

removeLast的实现基本和removeFirst对称:

1
2
3
4
5
6
复制代码public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}

主要实现还是借助于unlinkLast:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item; //保存最后一个节点的值
final Node<E> prev = l.prev; //把最后一个节点的前驱节点记为prev,它将成为last节点
l.item = null;
l.prev = null; // help GC //l节点的item和prev都记为空
last = prev; //此时设置刚才记录的前驱节点为last
if (prev == null) //prev为空的话,说明要删除的l前面原来没节点,那么删了l,整个链表为空
first = null;
else
prev.next = null; //prev成为最后一个节点,没有后继节点
size--;
modCount++;
return element;
}

在指定index删除

1
2
3
4
复制代码public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}

首先也是检查index是否合法,否则抛出IndexOutOfBoundsException异常。
如果删除成功,返回删除的节点。
具体实现依赖于unlink,也就是unlink做实际的删除操作:

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
复制代码E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;

if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}

if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}

x.item = null;
size--;
modCount++;
return element;
}

删除一个节点

根据图和代码来看,删除一个节点有以下几步:

  1. 将删除的节点保存在element里,同时把要删除节点的前驱节点标记为prev,后继节点标记为next;
  2. 如果prev为空,那么next节点直接为first节点,反之把prev的next指向next节点,如图中上面弯曲的红色箭头所示;
  3. 如果next为空,那么prev节点直接为last节点,反之把next的prev指向prev节点,如图中下面弯曲的蓝色箭头所示;
  4. 把要删除的节点置空,返回第一步保存的element。

还有种删除是以删除的元素作为参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
  • o为null,也是遍历链表,找到第一个值为null的节点,删除;
  • o部位空,遍历链表,找到第一个值相等的节点,调用unlink(x)删除。

####清空列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public void clear() {
// Clearing all of the links between nodes is "unnecessary", but:
// - helps a generational GC if the discarded nodes inhabit
// more than one generation
// - is sure to free memory even if there is a reachable Iterator
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}

由代码看出,也就是循环遍历整个链表,将每个节点的每个属性都置为空。

LinkedList还定义了很多别的方法,基本上和上面分析的几个函数功能类似

  • elemet和GetFirst一样,都返回列表的头,并且不移除它,如果列表为空,都会抛出NoSucnElement异常;
  • peek也会返回第一个元素,但是为空时返回null, 不抛异常;
  • remove方法内部就是调用removeFirst,所以表现相同,返回移除的元素,如果列表为空,都会抛出NoSucnElement异常;
  • poll也是移除第一个元素,只是会在列表为空时只返回null;
  • offer和offerLast在尾部add节点, 最终调用的都是addLast方法,offerFirst在头保护add节点,调用的就是addFirst方法;
  • peekFirst返回头节点,为空时返回null,peekLast返回尾节点,为空时返回null,都不会删除节点;
  • pollFirst删除并返回头节点,为空时返回null ,pollLast删除并返回尾节点,为空时返回null;
  • push和pop也是让LinkedList具有栈的功能,也只是调用了addFirst和removeFirst函数。

####ListIterator
最后重点说一下LinkedList中如何实现了ListIterator迭代器。
ListIterator是一个更加强大的Iterator迭代器的子类型,它只能用于各种List类的访问。尽管Iterator只能向前移动,但是ListIterator可以双向移动。它还可以产生相对于迭代器在列表中指向的当前位置的前一个和后一个元素的索引,可以用set()方法替换它访问过得最后一个元素。

定义如下:

1
2
3
4
复制代码public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}

指定index,可以在一开始就获取一个指向index位置元素的迭代器。
实际上LinkedList是实现了ListItr类:

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
复制代码private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;
private Node<E> next; //用于记录当前节点
private int nextIndex; //用于记录当前节点所在索引
private int expectedModCount = modCount;

ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index); //返回index处的节点,记录为next
nextIndex = index; //记录当前索引
}

public boolean hasNext() {
return nextIndex < size; //通过判断nextIndex是否还在size范围内
}

public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();

lastReturned = next; //记录上一次的值
next = next.next; //往后移动一个节点
nextIndex++; //索引值也加1
return lastReturned.item; //next会返回上一次的值
}

public boolean hasPrevious() { //通过哦按段nextIndex是否还大于0,如果<=0,就证明没有前驱节点了
return nextIndex > 0;
}

public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();

lastReturned = next = (next == null) ? last : next.prev; //往前移动
nextIndex--;
return lastReturned.item;
}

public int nextIndex() {
return nextIndex;
}

public int previousIndex() {
return nextIndex - 1;
}

public void remove() {
checkForComodification();
if (lastReturned == null)
throw new IllegalStateException();

Node<E> lastNext = lastReturned.next;
unlink(lastReturned);
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}

public void set(E e) {
if (lastReturned == null)
throw new IllegalStateException();
checkForComodification();
lastReturned.item = e;
}

public void add(E e) {
checkForComodification();
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}

public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (modCount == expectedModCount && nextIndex < size) {
action.accept(next.item);
lastReturned = next;
next = next.next;
nextIndex++;
}
checkForComodification();
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

我们可以发现在ListIterator的操作中仍然有checkForComodification函数,而且在上面叙述的各种操作中还是会记录modCount,所以LinkedList也是会产生快速失败事件的。

本文转载自: 掘金

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

和我一起读Java8 ArrayList源码

发表于 2017-12-13

集合系列文章:和我一起读Java8 LinkedList源码
首先放一张Java集合接口图:

14种容器接口

Collection是一个独立元素序列,这些元素都服从一条或多条规则,List必须按照插入的顺序保存元素,而Set不能有重复元素,Queue按照排队规则来确定对象产生的顺序。
List在Collection的基础上添加了大量的方法,使得可以在List的中间插入和移除元素。
有2种类型的List:

  • ArrayList 它长于随机访问元素,但是在List中间插入和移除元素较慢。
  • LinkedList 它通过代价较低的方式在List中间进行插入和删除,但是在随机访问方面相对较慢,但是它的特性急较ArrayList大。
    还有个第一代容器Vector,后面仅作比较。

下面正式进入ArrayList实现原理,主要参考Java8 ArrayList源码
####类定义

1
2
复制代码public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList 继承了AbstractList并且实现了List,所以具有添加,修改,删除,遍历等功能
实现了RandomAccess接口,支持随机访问
实现了Cloneable接口,支持Clone
实现了Serualizable接口,可以被序列化
####底层数据结构

1
2
复制代码transient Object[] elementData;  //存放元素的数组
private int size; //ArrayList实际存放的元素数量

ArrayList的底层实际是通过一个Object的数组实现,数组本身有个容量capacity,实际存储的元素个数为size,当做一些操作,例如插入操作导致数组容量不够时,ArrayList就会自动扩容,也就是调节capacity的大小。

####初始化
指定容量大小初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}

初始化一个指定容量的空集合,若是容量为0,集合为空集合,其中
private static final Object[] EMPTY_ELEMENTDATA = {};,容量也为0。

无参数初始化

1
2
3
4
5
6
复制代码/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

无参数初始化,其实DEFAULTCAPACITY_EMPTY_ELEMENTDATA的定义也为:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};与EMPTY_ELEMENTDATA的区别是当第一个元素被插入时,数组就会自动扩容到10,具体见下文说add方法时的解释。

集合作为初始化参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}

构造一个包含指定 collection 的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列的。

####size和IsEmpty
首先是两个最简单的操作:

1
2
3
复制代码public int size() {
return size;
}
1
2
3
复制代码public boolean isEmpty() {
return size == 0;
}

都是依靠size值,直接获取容器内元素的个数,判断是否为空集合。

#####Set 和Get操作
Set和Get操作都是直接操作集合下标

1
2
3
4
5
6
7
复制代码public E set(int index, E element) {
rangeCheck(index);

E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
1
2
3
4
5
复制代码public E get(int index) {
rangeCheck(index);

return elementData(index);
}

在操作之前都会做RangeCheck检查,如果index超过size,则会报IndexOutOfBoundsException错误。
elementData的操作实际就是基于下标的访问,所以ArrayList 长于随机访问元素,复杂度为O(1)。

1
2
3
4
复制代码@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}

####Contain

1
2
3
复制代码public boolean contains(Object o) {
return indexOf(o) >= 0;
}

contains 函数基于indexOf函数,如果第一次出现的位置大于等于0,说明ArrayList就包含该元素, IndexOf的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}

ArrayList是接受null值的,如果不存在该元素,则会返回-1,所以contains判断是否大于等于0来判断是否包含指定元素。

####Add和Remove

1
2
3
4
5
复制代码public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

首先确保已有的容量在已使用长度加1后还能存下下一个元素,这里正好分析下用来确保ArrayList容量ensureCapacityInternal函数:

1
2
3
4
5
6
7
复制代码private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}

ensureExplicitCapacity(minCapacity);
}

这边可以返回看一开始空参数初始化,this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA, 空参数初始化的ArrayList添加第一个元素,上面的if语句就会调用,DEFAULT_CAPACITY定义为10,所以空参数初始化的ArrayList一开始添加元素,容量就变为10,在确定了minCapacity后,还要调用ensureExplicitCapacity(minCapacity)去真正的增长容量:

1
2
3
4
5
6
7
复制代码private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

这里modCount默默记录ArrayList被修改的次数, 然后是判断是否需要扩充数组容量,如果当前数组所需要的最小容量大于数组现有长度,就调用自动扩容函数grow:

1
2
3
4
5
6
7
8
9
10
11
复制代码private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); //扩充为原来的1.5倍
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

oldCapacity记录数组原有长度,newCapacity直接将长度扩展为原来的1.5倍,如果1.5倍的长度大于需要扩充的容量(minCapacity),就只扩充到minCapacity,如果newCapacity大于数组最大长度MAX_ARRAY_SIZE,就只扩容到MAX_ARRAY_SIZE大小,关于MAX_ARRAY_SIZE为什么是private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;,我文章里就不深究了,感兴趣的可以参考stackoverflow上的有关回答:
Why the maximum array size of ArrayList is Integer.MAX_VALUE - 8?

Add还提供两个参数的形式,支持在指定位置添加元素。

1
2
3
4
5
6
7
8
9
复制代码public void add(int index, E element) {
rangeCheckForAdd(index);

ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}

在指定位置添加元素之前,先把index位置起的所有元素后移一位,然后在index处插入元素。

在index=2处插入5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public E remove(int index) {
rangeCheck(index);

modCount++;
E oldValue = elementData(index);

int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}

Remove接口也很好理解了,存储index位置的值到oldView作为返回值,将index后面所有的元素都向前拷贝一位,不要忘记的是还要将原来最后的位置标记为null,以便让垃圾收集器自动GC这块内存。
还可以根据对象Remove:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

找到对象所在位置,调用FastRemove函数快速删除index位置上的元素,FastRemove也就是比remove(index)少了个边界检查。

#####clear

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码/**
* Removes all of the elements from this list. The list will
* be empty after this call returns.
*/
public void clear() {
modCount++;

// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;

size = 0;
}

由于Java有GC机制,所以不需要手动释放内存,只要将ArrayList所有元素都标记为null,垃圾收集器就会自动收集这些内存。

Add和Remove都提供了一系列的批量操作接口:

1
2
3
4
复制代码public boolean addAll(Collection<? extends E> c);
public boolean addAll(int index, Collection<? extends E> c);
protected void removeRange(int fromIndex, int toIndex) ;
public boolean removeAll(Collection<?> c) ;

相比于单文件一次只集体向前或向后移动一位,批量操作需要移动Collection 长度的距离。

####Iterator与fast_fail
首先看看ArrayList里迭代器是如何实现的:

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
复制代码private class Itr implements Iterator<E> {
int cursor; // 记录下一个返回元素的index,一开始为0
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount; //这边确保产生迭代器时,就将当前modCount赋给expectedModCount

public boolean hasNext() {
return cursor != size;
}

@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor; //访问元素的index
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1; //不断加1,只要不断调用next,就可以遍历List
return (E) elementData[lastRet = i]; //lastRet在这里会记录最近返回元素的位置
}

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
ArrayList.this.remove(lastRet); //调用List本身的remove函数,删除最近返回的元素
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount; //上面的Remove函数会改变modCount,所以这边expectedModCount需要更新
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

ArrayList里可以通过iterator方法获取迭代器,iterator方法就是new一个上述迭代器对象:

1
2
3
复制代码public Iterator<E> iterator() {
return new Itr();
}

那么我们看看Itr类的主要方法:

  • next :获取序列的下一个元素
  • hasNext:检查序列中是否还有元素
  • remove:将迭代器新近返回的元素删除

在next和remove操作之前,都会调用checkForComodification函数,如果modCount和本身记录的expectedModCount不一致,就证明集合在别处被修改过,抛出ConcurrentModificationException异常,产生fail-fast事件。

fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
一般多线程环境下,可以考虑使用CopyOnWriteArrayList来避免fail-fast。

本文转载自: 掘金

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

漫画算法:最小栈的实现

发表于 2017-12-13

小灰回忆起当时的情景……

题目:实现一个栈,带有出栈(pop),入栈(push),取最小元素(getMin)三个方法。要保证这三个方法的时间复杂度都是O(1)。

小灰的想法:

1.创建一个整型变量 min,初始值-1

2.当第一个元素进栈时,让min=0,即把唯一的元素当做最小值。

3.之后每当一个新元素近栈,让新元素和min指向位置的元素比较大小。如果Stack[min]大于新元素,则min等于新元素的下标;Stack[min]小于新元素,则不做改变。

4.当调用getMin方法的时候,直接返回min所指向位置的元素即可。

按这个思路,近栈、出栈、取最小值的时间复杂度都是O(1),空间复杂度也是O(1)。

回忆到此结束……

解法:

1.设原有的栈叫做栈A,此时创建一个额外的栈B,用于辅助原栈A。

2.当第一个元素进入栈A的时候,让新元素的下标进入栈B。这个唯一的元素是栈A的当前最小值。(考虑到栈中元素可能不是类对象,所以B栈存储的是A栈元素的下标)

3.每当新元素进入栈A时,比较新元素和栈A当前最小值的大小,如果小于栈A当前最小值,则让新元素的下标进入栈B,此时栈B的栈顶元素就是栈A当前最小值的下标。

4.每当栈A有元素出栈时,如果出栈元素是栈A当前最小值,则让栈B的栈顶元素也出栈。此时栈B余下的栈顶元素所指向的,是栈A当中原本第二小的元素,代替刚才的出栈元素成为了栈A的当前最小值。(备胎转正)

5.当调用getMin方法的时候,直接返回栈B的栈顶所指向的栈A对应元素即可。

这个解法中近栈、出栈、取最小值的时间复杂度都是O(1),最坏情况空间复杂度是O(N)。

扩展题目:

实现一个队列,带有出队(deQueue),入队(enQueue),取最小元素(getMin)三个方法。要保证这三个方法的时间复杂度都尽可能小。

喜欢本文的朋友们,欢迎长按下图关注订阅号程序员小灰,收看更多精彩内容

本文转载自: 掘金

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

如何理解Pandas 和 Numpy里的axis

发表于 2017-12-13

简述一种如何直观的理解Pandas 和 Numpy里面的axis参数的方法。

Numpy 和 Pandas里的sort、mean、drop等操作,不是分行或者列分别用一个method来定义,而是一个method里面用户指定axis来操作的,举例来说:

我们先在此处下载了一份各国酒类消费的csv文件为例。
Screen Shot 2017-12-12 at 18.46.14
如下是pandas里按axis
0和1进行drop的操作示例,我们很容易看出,axis 0是按行drop,而axis 1是按列drop:
Screen Shot 2017-12-12 at 18.46.22

但是,mean操作呢?
Screen Shot 2017-12-12 at 18.49.18

容易看出,axis 0得出了每一列的均值,而axis 1得出了则是每一行的均值。那么,在Numpy里呢?

Screen Shot 2017-12-12 at 19.06.17

容易看出,axis为1的时候得出的是每行的sum,axis为0的时候得出了每列的sum。

由上面的例子,我们似乎可以看出,axis为1代表水平方向上的操作,axis为0代表垂直方向上的操作,比如axis为1的sum得出的就是每一行的和。

但是,在Pandas的Dataframe里面,为什么axis=1代表的是drop整个列呢?以下这个例子也可以说明一些情况:

Screen Shot 2017-12-12 at 18.56.53

联系这个视频How do I use the “axis” parameter in pandas? - YouTube,大家也可以得到一些结论,作者说:

0 is the row axis, and 1 is the column axis. When you drop with axis=1, that means drop a column. When you take the mean with axis=1, that means the operation should “move across” the column axis, which produces row means.指的就是一种更加容易理解的方式,“0就是行的axis,1就是列的axis,当以axis=1来drop,那么就是drop一个column,而axis=1
来取mean,那么就是这个操作‘穿越’了列的axis,产生了行上的mean”。

另外,其实我们也可以这样来操作,
Screen Shot 2017-12-12 at 19.01.27
Screen Shot 2017-12-12 at 19.01.45

可以看出,axis=0与axis=’rows’是一样的(在Pandas里),是不是更加容易理解了?

本文转载自: 掘金

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

1…906907908…956

开发者博客

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