Skip to main content

· 2 min read
CheverJohn

1.HashMap不是线程安全的。

HashMap是Map接口的子类,是将键映射到值的对象,其中键和值都是对象,并且不能包含重复键,但可以包含重复值。

HashMap允许nullkey和null value,而hashtable不允许。

2.HashTable是线程安全的

HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别在于HashMap允许空键值,由于非线程安全,效率上可能高于Hashtable。

HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。

HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因为contains方法容易让人引起误解。

最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap就必须要为止提供外同步了。

Hashtable和HashMap采用的hash/rehash算法大致都一样,所以性能上不会有很大的差别。

· 20 min read
CheverJohn

序言

这个我随手整到的一个知识点,确实很值得深挖,我在这其中得到的知识点也远大于其本身,这一点知识让我明白了任何小知识都得深挖源码,收获太大了。

首先感谢一下带给我思路的知乎答者:https://www.zhihu.com/question/28414001

下面正式开始我的表演

首先(看结果)

以一段代码开始

package cn.mr8god.kchaptereleven;

import java.util.HashSet;
import java.util.Random;
import java.util.Set;

/**
* @author Mr8god
* @date 2020/4/22
* @time 20:37
*/
public class SetOfInteger {
public static void main(String[] args) {
Random rnd = new Random(47);
Set<Integer> intset = new HashSet<Integer>();
for (int i = 0; i < 10000; i++){
intset.add(rnd.nextInt(30));
}
System.out.println(intset);
}
}

这是一段平平无奇的,展示HashSet魅力的一段代码。代码的意思也很简单,就是让随机生成的0~29的数字存入我们的Set当中去。这里边随机了10000次,就意味着有很多很多重复的数字,当然我们大Set家族绝对不会包容两个一模一样的玩意儿,所以输出结果很是理想(单指这方面的理想),它就输出了这三十个数字

[0, 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]

但是呢,结果却好像有点违背了我们所讲的“无序”,这明明是有顺序的一个一个排着输出的呀,啷个嘞就给我说成“无序”了呢?顺便展示一下概念

HashSet:一种没有重复元素的无序集合

我先来回答这个问题:我们一般所说的HashSet是无序的,但是它既不能保证存储和取出顺序一致, 更不能保证自然顺序一致(按照a-z)

顺道一提,我《Thinking in Java》书本中的输出是这样的

 [15, 8, 23, 16, 7, 22, 9, 21, 6, 1 , 29 , 14, 24, 4, 19, 26, 11, 18, 3, 12, 27, 17, 2, 13, 28, 20, 25, 10, 5, 0]

现在是不是就很奇妙了,为啥我同一段代码运行出来会是两个不同的结果,一个“有序”,一个“无序“。其实按照我江某人在C++中的经验来看,这很有可能就是语言版本的问题,这边很有可能就是JDK版本的问题,于是我们来分析源码验证我的思路

然后(分析源码)

我们首先从程序的第一步——集合元素的存储开始看起,先看HashSetadd方法的源码:

// HashSet 源码节选-JKD8
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

我们可以看到,HashSet直接调用的是HashMapput方法,并且将元素e放到mapkey位置(保证了唯一性)

顺着线索继续查看我们的HashMapput方法源码:

//HashMap 源码节选-JDK8
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

而我们的值在返回前需要经过HashMap中的hash方法

接着定位到hash方法的源码

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

hash方法的返回结果中是一句三目运算符,键(key)为null即返回0,存在则返回后一句的内容

(h = key.hashCode()) ^ (h >>> 16)

重点来了,这个东西叫做“扰动函数

这个时候断一下我们再来分析一下

hashCodeObject类中的一个方法,在子类中一般会被重写,而根据我们之前自己给出的程序,暂以Integer类型为例,我们来看一下IntegerhashCode方法的源码

    /**
* Returns a hash code for this {@code Integer}.
*
* @return a hash code value for this object, equal to the
* primitive {@code int} value represented by this
* {@code Integer} object.
*/
@Override
public int hashCode() {
return Integer.hashCode(value);
}

/**
* Returns a hash code for a {@code int} value; compatible with
* {@code Integer.hashCode()}.
*
* @param value the value to hash
* @since 1.8
*
* @return a hash code value for a {@code int} value.
*/
public static int hashCode(int value) {
return value;
}

果然,不出所料,这边真的重写hashCode了,IntegerhasCode方法的返回值就是这个数本身

注释:其实整数的值因为与整数本身一样唯一,所以它也是一个足够好的散列值呢

这上面得出的结论就是,下面的A式B式是等价的

A:(h = key.hashCode()) ^ (h >>> 16)
B:key ^ (h >>> 16)

over,继续回归正途,接下来就是直接进行位运算层面了

接着(转攻计算散列值——位运算开始了)

首先不急,先理清思路,这个时候

HashSet因为底层使用了哈希表(链表结合数组)实现,存储key时可以通过一系列运算后得出自己在数组中所处的位置。

我们在hashCode方法中返回到了一个等同于本身值的散列值(证明过程如“然后(分析源码)”中的“这个时候断一下我们再来分析一下”可见)。

但是呢,考虑到int类型数据的范围:-2147483648~2147483647,很明显,这些散列值不能够直接使用,因为内存是没有办法放得下一个40亿长度的数组的。所以它使用了对数组长度进行取模运算的解决方法,得余后再作为其数组下标。

JDK7中,这个被称为indexFor()的方法就是用来做这个的。 在JDK8中,就是一句代码,其实和JDK7的一样

//JDK8中
(tab.length - 1) & hash;
//JDK7中 
bucketIndex = indexFor(hash, table.length);
static int indexFor(int h, int length) {
return h & (length - 1);
}

顺道加一句,为什么我们取模运算不用%而用&呢?因为位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度会非常快,这样就导致了位运算&效率要比取模运算%高很多。

看到这边我们就知道了,存储时key的下标位置是需要通过hash方法和indexFor()JDK8的类indexFor()运算得来的。

正式开始我们的位运算(&)

我们开始举个例子了

HashMap中初始长度为16,length - 1 = 15;其二进制表示为 00000000 00000000 00000000 00001111

而与运算计算方式为:遇0则0,我们随便举一个key

    1111 1111 1010 0101 1111 0000 0011 1100 
& 0000 0000 0000 0000 0000 0000 0000 1111
----------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 1100

上面随便举的key值就是:1111 1111 1010 0101 1111 0000 0011 1100

我们将这32位从中分开,左边16位称作高位,右边16位称作低位,可以看到经过&运算后 结果就是高位全部归0,剩下了低位的最后四位。但是问题就来了,我们按照当前初始长度为默认的16,HashCode值为下图两个,可以看到,在不经过扰动计算时,只进行与(&)运算后 Index值均为 12 这也就导致了哈希冲突

需要的.jpg

哈希冲突的简单理解:计划把一个对象插入到散列表(哈希表)中,但是发现这个位置已经被别人的对象给占据了

例子中,两个不同的HashCode值却经过运算后,得到了相同的值,也就代表,他们都需要被放在下标为2的位置

一般来说,如果数据分布比较广泛,而且存储数据的数组长度比较大,那么哈希冲突就会比较少,否则很高。

但是,如果像上例中只取最后几位的时候,这可不是什么好事,即使我的数据分布很散乱,但是哈希冲突仍然会很严重。

别忘了,我们的扰动函数还在前面搁着呢,这个时候它就要发挥强大的作用了,还是使用上面两个发生了哈希冲突的数据,这一次我们加入扰动函数再进行与(&)运算

对哈希冲突的解决2.jpg

补充 :>>> 按位右移补零操作符,左操作数的值按右操作数指定的为主右移,移动得到的空位以零填充 ^ 位异或运算,相同则0,不同则1

可以看到,本发生了哈希冲突的两组数据,经过扰动函数处理后,数值变得不再一样了,也就避免了冲突

其实在扰动函数中,将数据右位移16位,哈希码的高位和低位混合了起来,这也正解决了前面所讲 高位归0,计算只依赖低位最后几位的情况, 这使得高位的一些特征也对低位产生了影响,使得低位的随机性加强,能更好的避免冲突

再然后

到了这里,我们一步步研究到了这一些知识

HashSet add() → HashMap put() → HashMap hash() → HashMap (tab.length - 1) & hash;

有了这些知识的铺垫,我对于刚开始自己举的例子又产生了一些疑惑,我使用for循环添加一些整型元素进入集合,难道就没有任何一个发生哈希冲突吗,为什么遍历结果是有序输出的,经过简单计算 2 和18这两个值就都是2

//key = 2,(length -1) = 15 

h = key.hashCode() 0000 0000 0000 0000 0000 0000 0000 0010
h >>> 16 0000 0000 0000 0000 0000 0000 0000 0000
hash = h^(h >>> 16) 0000 0000 0000 0000 0000 0000 0000 0010
(tab.length-1)&hash 0000 0000 0000 0000 0000 0000 0000 1111
0000 0000 0000 0000 0000 0000 0000 0010
-------------------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 0010

//2的十进制结果:2
//key = 18,(length -1) = 15

h = key.hashCode() 0000 0000 0000 0000 0000 0000 0001 0010
h >>> 16 0000 0000 0000 0000 0000 0000 0000 0000
hash = h^(h >>> 16) 0000 0000 0000 0000 0000 0000 0001 0010
(tab.length-1)&hash 0000 0000 0000 0000 0000 0000 0000 1111
0000 0000 0000 0000 0000 0000 0000 0010
-------------------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 0010

//18的十进制结果:2

按照我们上面的知识,按理应该输出 1 2 18 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 但却仍有序输出了

这就非常苦恼了,不过我发现了一个有趣的现象,当我的代码如下时

package cn.mr8god.kchaptereleven;

import java.util.HashSet;
import java.util.Random;
import java.util.Set;

/**
* @author Mr8god
* @date 2020/4/22
* @time 20:37
*/
public class SetOfInteger {
public static void main(String[] args) {
Random rnd = new Random(47);
Set<Integer> intset = new HashSet<Integer>();
for (int i = 0; i < 3; i++){
intset.add(rnd.nextInt(30));
}
System.out.println(intset);
}
}

输出结果是

[5, 8, 13]

于是我将问题的核心转为了数组长度问题,这个就是最终的大Boss了

最后(大Boss——数组长度)

继续找到HashMap源码,我发现了一个有趣的东西

    /**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

<< :按位左移运算符,做操作数按位左移右错作数指定的位数,即左边最高位丢弃,右边补齐0,计算的简便方法就是:把 << 左面的数据乘以2的移动次幂 为什么初始长度为16:1 << 4 即 1 * 2 ^4 =16;

这里边有一个叫做加载因子的东西,他默认值为0.75f,这是什么意思呢,我们来补充一点它的知识:

加载因子就是表示哈希表中元素填满的程度,当表中元素过多,超过加载因子的值时,哈希表会自动扩容,一般是一倍,这种行为可以称作rehashing(再哈希)。 加载因子的值设置的越大,添加的元素就会越多,确实空间利用率的到了很大的提升,但是毫无疑问,就面临着哈希冲突的可能性增大,反之,空间利用率造成了浪费,但哈希冲突也减少了,所以我们希望在空间利用率与哈希冲突之间找到一种我们所能接受的平衡,经过一些试验,定在了0.75f

现在可以解决我们上面的疑惑了

数组初始的实际长度 = 16 * 0.75 = 12

这代表当我们元素数量增加到12以上时就会发生扩容,当我们上例中for循环添加0-18, 这19个元素时,先保存到前12个到第十三个元素时,超过加载因子,导致数组发生了一次扩容,而扩容以后对应与(&)运算的(tab.length-1)就发生了变化,从16-1 变成了 32-1 即31

我们来算一下

//key = 2,(length -1) = 31 
h = key.hashCode() 0000 0000 0000 0000 0000 0000 0001 0010
h >>> 16 0000 0000 0000 0000 0000 0000 0000 0000
hash = h^(h >>> 16) 0000 0000 0000 0000 0000 0000 0001 0010
(tab.length-1)&hash 0000 0000 0000 0000 0000 0000 0011 1111
0000 0000 0000 0000 0000 0000 0000 0010
-------------------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 0010

//十进制结果:2
//key = 18,(length -1) = 31 
h = key.hashCode() 0000 0000 0000 0000 0000 0000 0001 0010
h >>> 16 0000 0000 0000 0000 0000 0000 0000 0000
hash = h^(h >>> 16) 0000 0000 0000 0000 0000 0000 0001 0010
(tab.length-1)&hash 0000 0000 0000 0000 0000 0000 0011 1111
0000 0000 0000 0000 0000 0000 0000 0010
-------------------------------------------------------------
0000 0000 0000 0000 0000 0000 0001 0010

//十进制结果:18

当length - 1 的值发生改变的时候,18的值也变成了本身。

到这里,才意识到自己之前用2和18计算时 均使用了 length -1 的值为 15是错误的,当时并不清楚加载因子及它的扩容机制,这才是导致提出有问题疑惑的根本原因。

总结

JDK7JDK8,其内部发生了一些变化,导致在不同版本JDK下运行结果不同,根据上面的分析,我们从HashSet追溯到HashMaphash算法、加载因子和默认长度。

由于我们所创建的HashSetInteger类型的,这也是最巧的一点,Integer类型hashCode()的返回值就是其int值本身,而存储的时候元素通过一些运算后会得出自己在数组中所处的位置。由于在这一步,其本身即下标(只考虑这一步),其实已经实现了排序功能,由于int类型范围太广,内存放不下,所以对其进行取模运算,为了减少哈希冲突,又在取模前进行了,扰动函数的计算,得到的数作为元素下标,按照JDK8下的hash算法,以及load factor及扩容机制,这就导致数据在经过 HashMap.hash()运算后仍然是自己本身的值,且没有发生哈希冲突。

补充:对于有序无序的理解

集合所说的序,是指元素存入集合的顺序,当元素存储顺序和取出顺序一致时就是有序,否则就是无序。

并不是说存储数据的时候无序,没有规则,当我们不论使用for循环随机数添加元素的时候,还是for循环有序添加元素的时候,最后遍历输出的结果均为按照值的大小排序输出,随机添加元素,但结果仍有序输出,这就对照着上面那句,存储顺序和取出顺序是不一致的,所以我们说HashSet是无序的,虽然我们按照123的顺序添加元素,结果虽然仍为123,但这只是一种巧合而已。

所以HashSet只是不保证有序,并不是保证无序

· 8 min read
CheverJohn

自动装箱与拆箱的定义

装箱就是自动将基本数据类型转换为包装器类型;拆箱就是,自动将包装器类型转换为基本数据类型

Java中的数据类型分为两类:一类是基本数据类型,另一类是引用数据类型

基本数据类型的分类.jpg

简单类型二进制位数封装器类
int32Integer
byte8Byte
long64Long
float32Float
double64double
char16Character
boolean1Boolean

上自动装箱代码

public static void main(String[] args) {
// TODO Auto-generated method stub
int a=3;
//定义一个基本数据类型的变量a赋值3
Integer b=a;
//b是Integer 类定义的对象,直接用int 类型的a赋值
System.out.println(b);
//打印结果为3
}

上面代码中的Integer b = a;就是我们所说的自动装箱的过程,上面代码在执行的时候调用了Integer.valueOf(int i)方法简化后的代码:

public static Integer valueOf(int i) {       
if (i >= -128 && i <= 127)
return IntegerCache.cache[i + 127];
//如果i的值大于-128小于127则返回一个缓冲区中的一个Integer对象
return new Integer(i);
//否则返回 new 一个Integer 对象
}

可以看到Integer.valueOf(a)其实是返回了一个Integer的对象。因此由于自动装箱的存在Integer b = a这段代码是没有问题的,并且我们可以简化的来这样写:Integer b = 3;

同样也等价于这样写:Integer b = Integer.valueOf(3)。

上自动拆箱代码

public static void main(String[] args) {
// TODO Auto-generated method stub

Integer b=new Integer(3);
//b为Integer的对象
int a=b;
//a为一个int的基本数据类型
System.out.println(a);
//打印输出3。
}

上面有一个:int a = b; 代码中把一个对象赋给了基本类型。其实这就等于int a = b.intValue()。

根据源码中可知道intValue是什么

public int intValue() {
return value;
}

这个方法就是返回了value值嘛,但是这里的value又是怎么一回事呢?继续找源码:

public Integer(int value) {
this.value = value;
}

原来这里的value就是,Integer后边括号里的值呀,于是我们的拆箱代码其实本质上是这样写的:

public static void main(String[] args) {
// TODO Auto-generated method stub

Integer b=new Integer(3);
//b为Integer的对象
int a=b.intValue();
//其中b.intValue()返回实例化b时构造函数new Integer(3);赋的值3。
System.out.println(a);
//打印输出3。
}

范围概念

这里边是一个挺重要的知识点,至少我之前看的疯狂Java视频资料,以及我看的《Java编程思想》这本书,都有这方面的介绍。先看一个代码哈:

public static void main(String[] args) {
Integer a = 1000,b=1000;
Integer c=100,d=100;
System.out.println(a==b);
System.out.println(c==d);
}

原本我会以为是输出的是:true true啦,但是实际上不对,正确答案是false true。为甚呢?细细道来。

public static void main(String[] args) {        
//1
Integer a=new Integer(123);
Integer b=new Integer(123);
System.out.println(a==b);//输出 false

//2
Integer c=123;
Integer d=123;
System.out.println(c==d);//输出 true

//3
Integer e=129;
Integer f=129;
System.out.println(e==f);//输出 false
//4
int g=59;
Integer h=new Integer(59);
System.out.println(g==h);//输出 true
}

常量池.jpg

第一部分输出false,很好理解,因为比较的是堆中指向的对象是不是同一个嘛,a,b是栈中对象的引用分别指向堆中的两个不同的对象。而a==b这条语句就是判断a、b在堆中指向的对象是不是统一个,因此输出为false。

第二部分输出true也很好理解,正是用了我们的自动装箱技术

我带大家这次仔细的看自动装箱的源码

public static Integer valueOf(int i) {       
if (i >= -128 && i <= 127)
return IntegerCache.cache[i + 127];
//如果i的值大于-128小于127则返回一个缓冲区中的一个Integer对象
return new Integer(i);
//否则返回 new 一个Integer 对象
}

上面的代码中:IntegerCache.cache[i + 127]; 表示狠眼生,继续看代码:

 private static class IntegerCache {

static final Integer cache[];
//定义一个Integer类型的数组且数组不可变
static {
//利用静态代码块对数组进行初始化。
cache = new Integer[256];
int j = -128;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}

//cache[]原来是一个Integer 类型的数组(也可以称为常量池),value 从-128到127,
public static Integer valueOf(int i) {
if (i >=-128 && i <= 127)
return IntegerCache.cache[i + (-IntegerCache.low)];
//如果装箱时值在-128到127之间,之间返回常量池中的已经初始化后的Integer对象。
return new Integer(i);
//否则返回一个新的对象。
}
}

原来IntegerCache类在初始化的时候,生成了一个大小为256的integer类型的常量池,并且integer.val的值从-128~127,当我们运行Integer c = a(临时做的一个小栗子哈)的时候,如果-128 <= a <= 127,就不会再生成新的integer对象。于是我们第二部分的c和d指向的是同一个对象,所以比较的时候是相等的,所以我们输出true。

第三部分,理解如第二部分

第四部分:代码中g指向的是栈中的变量,h指向的是堆中的对象,但是我们的g == h为什么还是true呢?这就是自动插箱干的好事,g == h这代码执行的时候就是:g == h.IntValue(),而h.IntValue()=59,所以两边其实是两个int在比较而已。

总结

简单一句话:

装箱就是自动将基本数据类型转换为包装器类型;

拆箱就是自动将包装器类型转换为基本数据类型。

· 11 min read
CheverJohn

本文灵感来源于知乎文章(https://zhuanlan.zhihu.com/p/62779357) 以及《Java编程思想》P85页

使用this关键字之前

Java提供了一个叫做this关键字,this关键字总是指向调用该方法的对象。根据this出现的位置不同,this作为对象的默认引用有两种情况。

  1. 构造器中引用该构造器正在初始化的对象
  2. 在方法中引用调用该方法的对象

在方法中引用调用该方法的对象

代码示例

那我们直接上了概念,肯定是不能够被大家理解的哈,我们转念换个角度来想一想,我们如果没有this关键字,会面临一个什么样子的情况呢?

public class Person {
//定义一个move()方法
public void move(){
System.out.println("正在执行move()方法");
}
//定义一个eat()方法,eat()方法需要借助move()方法
public void eat(){
Person p = new Person();
p.move();
System.out.println("正在执行eat()方法");
}
public static void main(String[] args) {
//创建Person对象
Person p = new Person();
//调用Person的eat()方法
p.eat();
}
}
// 代码来源于:https://zhuanlan.zhihu.com/p/62779357

运行结果为:

正在执行move()方法
正在执行eat()方法

代码讲解

上述的方式确实能够做到eat()方法里调用move()方法,但是我们在main()方法里可以看到我们总共创建了两个对象:main()方法里创建了一个对象;eat()方法里创建了一个对象。但是实际上我们是不需要两个对象的,因为在程序调用第一个eat()方法时一定会提供一个Person对象,而不需要重新创建一个Person了。

因此我们可以通过this关键字在eat()方法中获得调用该方法的对象。this关键字只能在方法内部使用,表示对”调用方法的那个对象“的引用。

于是上面的代码Person类中的eat()方法改为下面这种方式较为合适:

//定义一个eat()方法,eat()方法需要借助move()方法
public void eat(){
//使用this引用调用eat()方法的对象
this.move();
System.out.println("正在执行eat()方法");
}

不过呢,虽然接下来要说的,可能会让读者同学感觉我是在耍你哈。但是可不是哦,上面这么多我只是做个引子而已,用来引导大家的。我先说我要说的知识点吧

this关键字的用法和其他对象引用并无不同。但是如果要在方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可。当前方法的this引用会自动应用用途同一类中的其他方法。所以上述代码也可以这样写:

//定义一个eat()方法,eat()方法需要借助move()方法
public void eat(){
move();
System.out.println("正在执行eat()方法");
}

整体代码可以如下

package cn.mr8god.example;

/**
* @author Mr8god
* @date 2020/4/14
* @time 16:09
*/

public class Person {
public void move(){
System.out.println("正在执行move()方法");
}

public void eat(){
move();
System.out.println("正在执行eat()方法");
}

public static void main(String[] args) {
Person p = new Person();
p.eat();
}
}

暂时的小总结

​ 在eat()方法内部,你可以写this.move(),但无此必要。编译器能够帮你自动添加。只有当明确指出对当前对象的引用时,才需要使用this关键字。例如,当需要返回对当前对象的引用的时候,就常常在return语句里这样写:

package cn.mr8god.chapterfive;

/**
* @author Mr8god
* @date 2020/4/14
* @time 11:18
*/
public class Leaf {
int i = 0;
Leaf increment(){
i++;
return this;
}
void print(){
System.out.println("i = " + i);
}

public static void main(String[] args) {
Leaf x = new Leaf();
x.increment().increment().increment().print();
}
}

代码中,由于increment()通过this关键字返回了对当前对象的引用,所以很容易就可以在一条语句里对同一个对象执行多次操作。

this关键字对于将当前对象传递给其他方法也很有用

package cn.mr8god.chapterfive;

/**
* @author Mr8god
* @date 2020/4/14
* @time 11:21
*/

class Person{
public void eat(Apple apple){
Apple peeled = apple.getPeeled();
System.out.println("Yummy");
}
}

class Peeler{
static Apple peel(Apple apple){
return apple;
}
}
class Apple{
Apple getPeeled(){ return Peeler.peel(this); }
}
public class PassingThis {
public static void main(String[] args) {
new Person().eat(new Apple());
}

}

Apple需要调用Peeler.peel()方法,这个方法是一个外部的工具方法,将执行由于某种原因而必须放在Apple外部的操作。为了将其自身传递给外部方法,Apple必须使用this关键字。

在构造器中调用构造器

另一种情形是:this关键字可以用于构造器中作为默认引用,由于构造器是直接使用new关键字来调用的,而不是使用对象来调用的,所以this在构造器中代表该构造器正在初始化的对象。

例一

package cn.mr8god.example;

/**
* @author Mr8god
* @date 2020/4/14
* @time 17:05
*/
public class Person {
public int age;
public Person(){
int age = 0;
this.age = 3;
}

public static void main(String[] args) {
System.out.println(new Person().age);
}
}

与普通方法类似,大部分时候,我们在构造器中访问其他成员变量和方法时都可以省略this前缀,但是如果构造器中有一个与成员变量同名的局部变量,又必须在构造器中访问这个被覆盖的成员变量,则必须使用this前缀。正如上面的程序所示。

this作为对象的默认引用使用时,程序可以像访问普通引用变量一样来访问这个this引用,甚至可以把this当成普通方法的返回值。如下面的程序

public class Person {
public int age;
public Person grow() {
age ++;
return this;
}
public static void main(String[] args) {
Person p = new Person();
//可以连续调用同一个方法
p.grow().grow().grow();
System.out.println("p对象的age的值是:"+p.age);
}
}

运行结果为:

p对象的age的值是:3

上面的代码中可以看到,如果在某个方法中把this作为返回值,则可以多次连续调用同一个方法,从而使得代码变得更加的简洁。

例二

有时候为一个类写了多个构造器,我们可能想在一个构造器中调用另一个构造器,以避免重复代码。可以使用this关键字做到这一点。

package cn.mr8god.chapterfive;

import static net.mindview.util.Print.print;

/**
* @author Mr8god
* @date 2020/4/14
* @time 11:31
*/
public class Flower {
int petalCount = 0;
String s = "initial value";
Flower(int petals){
petalCount = petals;
print("Constructor w/ int arg only, petalCount= "
+ petalCount);
}
Flower(String ss){
print("Constructor w/ String arg only, s = " + ss);
s = ss;
}
Flower(String s, int petals){
this(petals);
this.s = s;
print("String & int args");
}
Flower(){
this("hi", 47);
print("default constructor (no args)");
}
void printPetalCount(){
print("petalCount = " + petalCount + " s = " + s);
}

public static void main(String[] args) {
Flower x = new Flower("江晨玥",222);
// Flower x = new Flower();
x.printPetalCount();
}
}

这串代码如果给初学者看的话,会有一点不明确的地方,毕竟代码太长了嘛,这边给个建议,就只看我们的this指针部分哦!

然后代码中,我选择初始化了一个Flower("江晨玥", 222)的方法,首先这行代码会被用到上面的Flower(String s, int petals)里边,很好的实现了方法的重载嘛。然后我们就可以直观地看到这边,我讲解几个可能会有疑问的地方哈

可能会有疑问一

这边

Flower(String s, int petals){
this(petals);
this.s = s;
print("String & int args");
}

代码中的this(petals)到底是怎么一回事,其实有这个疑问还是要算你的this关键字没有理解到家,this其实在这里指的是Flower,相当于我在这个Flower(String s, int petals)里引用了Flower(int petals)方法。

可能会有疑问二

这边的this.s = s,可能也会有疑问,其实这个也展示了this的另外一种用法。由于参数s的名称和数据成员s的名字相同,所以会很容易产生歧义。使用this.s就可以来代表数据成员解决这个问题。Java日常编程经常会这样的哦

讲解一

printPetalCount()

方法表明,除构造器之外,编译器禁止在其他任何方法中调用构造器,不信?你可以试试!

Over!

· 21 min read
CheverJohn

虽然我上面开了两篇博客准备一锅把函数端掉,但是当我想到数组哈,我觉得还是有必要对它好好搞一番。为什么呢?因为当我们在形参中放一个数组时,要开始注意了昂,这个数组是按照数组基本的操作传递值的,它是以指针的方式运转的!!!一提到指针哈,就得好好琢磨琢磨了,毕竟我江某人如今的观点是C++最重要的有两个:指针和STL库。

要我说,之前讲的两章,着实是对函数基础知识的总结。而我们日常使用中,是不会这么简单的。比如说我们企业中要计算某个项目中每个用户所购买的东西之和。我们很容易想到每个数组可以索引指向一个顾客,要计算总共有多少个东西被买掉了,我们可以使用循环来计算出总和。这不是不可以哈。但是呢,我们其实可以用一个函数来实现它。我们可以在声明函数时,放入一个数组形状的形参,如下:

void function( int arr[], int a)

很明显,这里边arr就是咱们即将要传递的数组,[]里边是空的,说明我们待会要传递的数组的长度是需要额外设置的。但是我再强调一点哈,这个arr实际上并不是数组,而是一个指针!但是呢,我们在编写函数的时候,是可以将arr看做是数组的。

example

下面是我写的一个小example

#include <iostream>
const int ArSize = 8;

int sums(int arr[], int n);

int main()
{
using namespace std;
int counts[ArSize] = { 1, 2, 4, 8, 16, 32, 64,128};

int sum = sums(counts, ArSize);
cout << "Total counts: " << sum << "\n";
return 0;
}

int sums(int arr[], int n){
int total = 0;
for(int i = 0; i < n; i++)
total = total + arr[i];
return total;
}

接下来我来详细讲一讲哈,其实我们当初在学习数组的时候就知道,数组名是可以当做指针来用的,数组名指向该数组的第一个元素的地址。但是呢,这边在函数中讨论数组和指针的话。我得把需要注意的几个点额外说一哈

  1. 数组声明使用数组名来表示存储位置
  2. 对数组使用sizeof得到的是整个数组的长度,举个例子哈,int aaa[8],int是4个字节,对这个数组使用sizeof的话,我们得到的长度为32位字节
  3. 如果我们使用取地址符&的话,我们得到的也会是一个长度为32字节的内存块的地址。

arr是咱们的数组名,根据C++规则,arr指代的是第一个元素的地址。 所以咱们的函数传递的也是地址哈。元素的类型是int,那么咱们的指针也应该是int类型的。因此,我们可以使用int *来表示。

int sum = sums(int * arr, int n)

易知,int * arr 替换了int arr[],这两个的含义是相同的。

但是呢,其实这两种表达方法也是有区别的,

数组表达式(int arr[])提醒咱们的程序员,arr不仅指向int,还指向了int数组的第一个int。当指针指向数组的第一个元素时,本书使用了数组表达法;

指针表达式可以用在当我们指针指向一个独立的值,而不是第一个值的时候。

!!!要记住只有在这边是可以的等价的,在其他地方都是不等价的哦。例如我们不能再函数体中将两者相替换。

经过我们的挖掘后,知道了arr数组名实际上是一个指针的事实后,我们也可以 用方括号数组表示法来访问数组元素。无论arr是指针还是数组名,表达式arr[3]都指的是数组的第4 个元素。

这边总结出两个式子,希望能记住:

arr[i] == *(ar + i)
&arr[i] == ar + i

在强调一点,指针加一的意义,指的是加上一个与指针指向的类型的长度。例如上文中的32字节内存块。对于遍历数组而言,使用指针加法和数组下标是等效的。

数组作为参数有啥意义呢?

讲得更加清楚一点,实际上数组内容并没有传递给函数,而是将数组的地址、包含的元素类型以及元素数目提交给了函数。有了这些信息后,函数便可以使用原来的数组。传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组

一种是拷贝了原始数据,并进行操作,一种是使用指针,直接操作原始数据。都实现了函数的值传递。但是我想两种方法肯定是有利有弊的呀。继续往下分析。

数组名与指针对应是否是好事呢?

先说回答哈,确实是一件好事。将数组地址作为参数可以节省复制整个数组所需要的时间和内存。如果数组很大的话。则使用拷贝的系统开销将非常大;程序不仅需要更多的计算机内存,还需要花费时间来复制大块的数据。

但是呢,有利有弊哈,我们使用指针其本质上时使用了原始数据,增加了破坏数据的风险。不过不怕,C++可以解决它,ANSI C也可以解决它,那就是const限定符了。稍后我在写例子哈。

这边再写一个例子,用于演示咱们的指针是如何运转的:

#include <iostream>
const int ArSize = 8;
int sums(int arr[], int n);
int main(){
int things[ArSize] = {1,2,4,8,16,32,64,128};
std::cout << things << " = arr address, ";

std::cout << sizeof(things) << " = sizeof things\n";
int sum = sums(things, ArSize);
std::cout << "Total things: " << sum << std::endl;
sum = sums(things, 3);
std::cout << "First tree people buy " << sum << " things.\n";
sum = sums(things + 4, 4);
std::cout << "Last four people buy " << sum << " things.\n";
return 0;
}

int sums(int arr[], int n){
int total = 0;
std::cout << arr << " = arr, ";
std::cout << sizeof(* arr) << " = sizeof arr\n ";
for (int i = 0; i < n; i++)
total += arr[i];
return total;
}

这边的地址值和数组的长度会随着系统的变化而变化哈,如果你和你的小伙伴们运行出不一样的结果,不要诧异哦!此外,有些C++实现 的是以十进制而不是十六进制格式显示地址哈,所以不要太大惊小怪,显得见识浅薄了些。还有一些编译器以十六进制显示地址时,会加上前缀0x呢。

代码说明:

首先我这边things和arr指向了同一个地址。但是sizeof(things)的值为32,而sizeof(arr)为4(是我电脑上运行的结果哈)。这是由于sizeof(things)是整个数组的长度,而sizeof(arr)只是指针变量的长度。顺道加一个知识点,这也是必须显式传递数组长度,而不能在sums()中使用sizeof(arr)的原因;指针本身并没有之处数组的长度。

因为咱们的sums()只能通过第二个参数获知数组中的元素数量,我们可以对函数做修改。例如,程序第二次使用该函数时,这样调用它:

sum = sums(things, 3);

通过告诉函数things有3个元素,可以让它计算前3个元素的总和。

也可以提供假的数组起始位置:

sum = sums(things + 4, 4);

由于things是第一个元素的地址,因此things+4是第五个元素的地址。这条语句将计算数组第5、6、7、8个元素的总和。请注意输出中第三次函数调用选择将不同于前两个调用的地址赋给arr的。

Attention!

我们可以数组类型和元素数量告诉数组处理函数,通过两个不同的参数来传递他们:

int sums(int * arr, int n)

而不要试图使用方括号表示法来传递数组长度:

int sums(int arr[size])

各种例子,来更深入了解数组函数

案例

假设要使用一个数组来记录房地产的价值。

思路分析

首先要明确使用哪种类型。当然double的取值范围比int和long大,并且提供了足够多的有效位数来精确地表示这些值。

接下来必须决定数组元素的数目。(这边不考虑动态数组)如果房地产数目不超过5个,则可以使用一个包含5个元素的double数组。

考虑操作:两个基本的操作,一、将值读入到数组中和显示数组内容。二、重新评估每种房地产的值。

简单起见,我们规定房地产以相同比率增加或者减少。

1.填充数组

顾客不止一个,所以我们可以做多个数组,房产得有上限,毕竟我这边不搞花里胡哨的动态数组。所以函数定义为:

int fill_array(double arr[], int limit);

该函数接受两个参数,一个是数组名,另一个指定了要读取的最大元素数;该函数返回实际读取的元素数。例如,如果使用该函数处理一个包含了5个元素的数组,则将5作为第二个参数。如果只输入3个值,则该函数将返回3.

可有循环连续地将值读入到数组中,但是我们该如何提早结束循环呢?有两种思路,一、使用一个特殊值来指定输出结束。由于所有的属性不为负,我们可以使用负值来指出输入结束。二、该函数应对错误输入做出反应,如停止输入等。代码如下:

int fill_array(double arr[], int limit){
using namespace std;
double temp;
int i;
for (i = 0; i < limit; i++)
{
cout << "Enter value # " << (i + 1) << ": ";
cin >> temp;
if (!cin)
{
cin.clear();
while (cin.get() != '\n')
continue;
cout << "Bad input; input process terminated.\n";
break;
}
else if (temp < 0)
break;
arr[i] = temp;
}
return i;
}

上面函数可以判断输入是否出错,比如说负值啦等等。如果输入正确的话,则循环将会在读取最大数目的值后结束。循环完成的最后一项工作后,将i加1,因此循环结束后,i将比最后一个数组索引大1,即等于填充的元素数目。然后,函数返回这个值。

2.显示数组及用const保护数组

不是啥高大的东西,就是显示元素的数组,但是最重要的东西应该是const保护数组。

当我们用数组名表示指针传递值时,会导致原始数据受到威胁。这个时候我呼应了上文中所要讲的方法const保护数组不被修改。

为了防止函数无意中修改数组的内容,我们可以在声明形参时使用关键字const:

void show_array(const double arr[], int n);

该声明表明,指针arr指向的是常量数据。这意味着不能使用arr修改该数据,也就是说,可以使用值,但是不会修改。咳咳,这并不是意味着原始数组必须是常量,而只是意味着不能在show_array()函数中使用arr来修改数据。因此该函数将数组视为只读数据。 如果你要在函数中给原数组赋值的话,是会报错的。

show_array函数代码如下:

void show_array(const double arr[], int n){
using namespace std;
for (int i = 0; i < n; i++)
{
cout << "Property #" << (i + 1) << ": $";
cout << arr[i] << endl;
}

}

3.修改数组

实现的功能是对数组中每个元素与同一个重新评估因子相乘。需要给函数传递3个参数:因子、数组和元素数目。该函数不需要返回值,因此代码如下:

void show_array(const double arr[], int n){
using namespace std;
for (int i = 0; i < n; i++)
{
cout << "Property #" << (i + 1) << ": $";
cout << arr[i] << endl;
}

}

这个就和上一个函数不一样了,这边是必须要修改值的,所以不能加const

4.组合代码解出题目

#include <iostream>
const int Max = 5;
int fill_array(double arr[], int limit);
void show_array(const double arr[], int n);
void revalue(double r, double arr[], int n);


int main(){
using namespace std;
double properties[Max];

int size = fill_array(properties, Max);
show_array(properties, size);
if (size > 0)
{
cout << "Enter revaluation factor: ";
double factor;
while (!(cin >> factor))
{
cin.clear();
while (cin.get() != '\n')
continue;
cout << "Bad input; Please enter a number: ";
}
revalue(factor, properties, size);
show_array(properties, size);
}
cout << "Done.\n";
cin.get();
cin.get();
return 0;
}

int fill_array(double arr[], int limit){
using namespace std;
double temp;
int i;
for (i = 0; i < limit; i++)
{
cout << "Enter value #" << (i + 1) << ": ";
cin >> temp;
if(!cin)
{
cin.clear();
while(cin.get() != '\n')
continue;
cout << "Bad input; input process terminated.\n";
break;
}else if (temp < 0)
break;
arr[i] = temp;
}
return i;
}


void show_array(const double arr[], int n){
using namespace std;
for (int i = 0; i < n; i++){
cout << "Property #" << (i + 1) << ": $";
cout << arr[i] << endl;
}
}

void revalue(double r, double arr[], int n){
for(int i = 0; i < n; i++)
arr[i] *= r;
}

5.程序说明

回顾一下整个过程。我们首先考虑的是通过数据类型和设计适当的函数来处理数据,然后讲这些函数组合成一个程序。有时这个也称为自下而上的程序设计(bottom-up programming),因为设计过程是从组建到整体进行。这种方法非常适合于OOP——它首先强调的是数据表示和操纵。

以前的过程性编程倾向于从上而下的程序设计,首先指定模块化设计方案,然后在研究细节,

最终产品都是模块化程序,也就是我们最后得到的东西都是模块化的东西,据我目前的经验来看,当代程序的思路都是模块化!

6.数组处理函数的常用编写方式

总结一下数组处理函数无非就两种情况

情况一:

void f_modify(double ar[], int n);

情况二:

void _f_no_change(const double ar[], int n);

再扯几句哈,函数原型是可以省略变量名的,也可以将返回类型作指定,比如这边就指定了void。

7.使用数组区间的函数

上面我们讲数组和函数的时候,用的是传统的C++方法,将指向数组起始处的指针作为一个参数,将数组长度作为第二个参数(指针指出数组的位置和数组类型),这样便给函数提供了找到所有数据所需要的信息。

我们处理数组的C++函数,必须将数组中的数据种类、数组的起始位置和数组中元素数量给函数。

还有一种给函数提供所需信息的方法是,即指定元素区间,这可以通过传递两个指针来完成:一个指针表示数组的开头,另一个指针标识数组的尾部。(C++标准模板库STL中将区间方法广义化了,STL方法使用“ 超尾”概念来指定区间,也就是说,对于数组而言,标识数组结尾的参数将是指向最后一个元素后面的指针。举个例子:

double elboud[20];

指针elboud和elboud+20定义了区间。唉,其实就是数组名+多少个(数字)从而做出区间,写个小例子便于理解:

#include <iostream>
const int ArSize = 8;
int sums(const int * begin, const int * end);
int main(){
using namespace std;
int things[ArSize] = {1, 2, 4, 8, 16, 32, 64, 128};

int sum = sums(things, things + ArSize);
cout << "Total things eaten: " << sum << endl;
sum = sums(things, things + 3);
cout << "First three people buy " << sum << " things.\n";
sum = sums(things + 4, things + 8);
cout << "Last four people buy " << sum << " things.\n";
return 0;
}

int sums(const int * begin, const int * end){
const int * pt;
int total = 0;

for(pt = begin; pt != end; pt++)
total += *pt;

return total;
}

太简单了,不解释了!

· 12 min read
CheverJohn

C++自带一个包含函数的大型库(标准ANSI库加上多个C++类),但真正的编程乐趣在于编写自己的函数;另一方面,要提高编程效率,可以更深入地学习STL和BOOSTC++提供的功能。

7.1 复习函数的基本知识

来复习一下介绍过的有关函数的知识。要使用C++函数,必须完成以下工作:

  • 提供函数的定义
  • 提供函数原型
  • 调用函数

库函数是已经定义好和编译好的函数,同时可以使用标准库头文件提供其原型,因此只需正确地调用这种函数即可。比如说strlen()函数,可以用来确定字符串的长度。相关的标准头文件cstring包含了strlen()和其他一些与字符串相关的函数的原型。

然后还有一点要注意的是,咱们程序员在编写函数的时候,一定要注意三点——定义、提供原型、调用。

#include <iostream>

void simple();

int main(){
using namespace std;
cout << "main() will call the simple() function:\n";
simple();
cout << "main() is finished with the simple() function.\n";
cin.get();
return 0;
}



void simple(){
using namespace std;
cout << "I'm but a simple function.\n";
}

上面就是一个小例子。

7.1.1 定义函数

可以将函数分为两类:没有返回值的函数和有返回值的函数。没有返回值的函数被称为void函数,其通用格式如下:

void functionName(parameterList){
statement(s)
return ;
}

其中,paramterList指定了传递给函数的参数类型和数量,本章后面将更详细地介绍该列表。可选的返回语句标记了函数的结尾;否则,函数将在右花括号处结束。

对于返回值,需要注意的是,

  • 如果原函数的数据类型是double但是返回的是int,返回值将会被强制转化为double
  • C++对返回值的类型有一定的限制:不能是数组,但可以是其他任何类型——整数、浮点数、指针、甚至可以是结构和对象(不过我们可以将数组作为结构或对象组成部分来返回)
  • 若函数包含多条返回语句(例如,它们位于不同的ifelse选项中)则函数在执行遇到的第一条返回语句后结束。

干货分享:虽然作为一名程序员不需要知道函数是怎么返回值的,但是对这个问题有所了解有助于澄清概念。

通常函数通过将返回值复制到指定的CPU寄存器或内存单元中来将其返回。

随后,调用程序将查看该存储单元。

返回函数和调用函数必须就该内存单元中存储的数据的类型达成一致。

函数原型将返回值类型告知调用程序,而函数定义命令被调用函数应返回什么类型的数据。

在原型中提供与定义中相同的信息似乎有些多余,但这样做确实有道理。要让信差从办公室的办公桌上取走一些物品,则向信差和办公室中的同事交代自己的意图,将提高信差顺利完成这项工作的概率。

7.1.2 函数原型和函数调用

咱们对函数调用这个知识点是很熟悉的,但是对函数原型并不清楚,函数原型通常隐藏在include文件中。

 函数声明由函数返回类型、函数名和形参列表组成。形参列表必须包括形参类型,但是不必对形参命名。这三个元素被称为函数原型,函数原型描述了函数的接口。

1.为什么需要原型

原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型(如果有的话)以及参数的类型和数量告诉编译器。

double volume = cube(side);

首先,原型告诉编译器,cube()有一个double 参数。如果程序没有提供这样的参数,原型将让编译器能够捕获这种错误。其次,cube()函数完成计算后,将把返回值放置在指定的位置——可能是CPU寄存器,也可能是内存中。然后调用函数(这里为main())将从这个位置取得返回值。由于原型指出了cube()的类型为double,因此编译器知道应检索多少个字节以及如何解释它们。如果没有这些信息,编译器将只能进行猜测,而编译器是不会这样做的。

编译器需要原型,因为这样将提高效率,让编译器在广大的文件里找有用的文件,是一件大海捞针的事儿,且编译器在搜索文件的时候将必须停止 对main()的编译。一个更严重的问题是,函数甚至可能并不在项目的文件中(C++允许将一个程序放在多个文件中,单独编译他们,然后再将它们组合起来),在这种情况下,可能导致编译器在编译main()时无权访问函数代码。如果函数位于库中,情况也将如此。

综上,避免使用函数原型的唯一办法就是,在首次使用函数之前定义它,但这并不总是可行的。

2.原型的语法

函数原型是一条语句,因此必须以分号结束。获取原型的方法就是,复制函数定义中的函数头,并添加分号即可。

3.原型的功能

可以极大地降低程序出错的几率。

原型应确保以下几点:

  • 编译器正确处理函数返回值
  • 编译器检查使用的参数数目是否正确
  • 编译器检查使用的参数类型是否正确。如果不正确,则转换为正确的类型(如果可能的话)。

讲一讲参数数目不对时将发生的情况

例如,假设进行了如下调用

double z = cube();

如果没有函数原型,编译器将允许它通过。当函数被调用时,它将找到cube()调用存放值的位置,并使用这里的值。这个正式C语言从C++借鉴原型之前的工作方式。对于ANSI C(美国国家标准规定的C语言)来讲,原型是可选的,因此有些C语言程序正是这样工作的。但在C++中,原型是不可选。因此保证了不会发生这种错误。

接下来,我们假设提供了一个参数,但是它的类型不正确。在C语言中,这将造成奇怪的错误,例如,如果函数需要一个int值(假设占16位),而程序员传递了一个double值(假设占64位),则函数将只检查64位中的前16位,并试图将它们解释为一个int值。但是C++自动将传递的值转换为原型中指定的类型,条件是两者都是算数类型。

这个时候我来举个例子吧

#include <iostream>
void cheers(int);
double cube(double x);
int main(){
using namespace std;
cheers(5);
cout << "Give me a number: ";
double side;
cin >> side;
double volume = cube(side);
cout << "A" << side << "-foot cube has a volume of ";
cout << volume << " cubic feet.\n";
cheers(cube(2));
return 0;
}

void cheers(int n){
using namespace std;
for(int i = 0; i < n; i++){
cout << "Cheers! ";
cout << endl;
}
}

double cube(double x){
return x * x * x;
}

这个是C++的代码

这个代码就能够应付下述语句中两次出现的类型不匹配的情况:

cheers(cube(2));

代码讲解:首先程序将int的值2传递给cube(),而后者期望的是double类型。编译器注意到,cube()原型指定了一个double类型参数,因此将2转化为2.0(一个double值)。接下来cube()返回一个double值(8.0),这个值被用作cheer()的参数。编译器将再一次检查原型,并发现cheer()要求一个int参数,因此它将返回值转换为整数8.通常原型自动将被传递的参数强制转换为期望的类型

​ 自动类型转换并不能避免所有可能的错误。例如,如果将8.33E27传递给期望一个int值的函数,则这样大的值将不能正确转换为int值。当较大的类型被自动转换为较小的类型时,有些编译器将发生警告,指出这可能丢失数据。

​ 仅当有意义的时候,原型才会导致类型转换。正如原型不会将整数转换为结构或指针。

在编译阶段进行的原型化被称为静态类型检查(static type checking)。可以看出,静态类型检查可捕获许多在运行阶段非常难以捕获的错误。

· 4 min read
CheverJohn

7.2 函数参数和按值传参

C++中经常性的一种操作就是,案值传递参数,这意味着将数值传递给一个函数,然后后者还会返回一个值赋给一个新的变量。

在函数中被声明的变量是函数私有的,这是他自己凭本事声明的变量。在函数被调用时,计算机会为他们(指变量)申请内存。在函数结束的时候,计算机会释放掉内存,这样的变量我们称之为局部变量

7.2.1 多个参数

函数可以包含多个参数,例如写成这种形式:

function('R', 25);

上述函数调用将两个参数传递给函数function( ),

同样,在定义函数时,也在函数头中使用由逗号分割的参数声明列表

void function(char c, int n)

之前写的function中R传递给了c,25传递给了n

7.2.2 另外一个接受两个参数的函数

注意了注意了,这边要讲一个比较有趣的东西了

Problem:美国许多州都采用某种纸牌游戏的形式来发行彩票,让参与者从卡片中选择一定数目的选项,例如从51个数字中选取6个,随后彩票管理者将随机抽取6个数,如果参与者选择的数字与这6个完全相同,将赢得大约几百万美元的奖金,我们的函数将计算中奖的几率

首先,需要一个公式。假设必须要从51个数中选取6个数,而获奖的几率是1/R,则R的计算公式如下 $$ R =\frac {51×50×49×48×47×46}{6×5×4×3×2×1} $$ 选择6个数时,分母为前6个整数的乘积或6的阶乘分子也是6个连续整数的乘积,从51开始依次递减一,推而广之,如果从numbers个数中选取picks个数,而分母是picks的阶乘,分子为numbers开始向前的picks个整数的乘积。可以用for循环进行计算:

long double result = 1.0
for (int n = numbers, p = picks; p > 0; n--, p--)
result = result * n / p;

循环不是首先将所有的分子项相乘,而是首先将1.0与第1个分子项相乘,然后除以第1个分母项,然后下一轮循环,乘以第2个分子项并除以第2个分母项,这样得到的乘积和先进行乘法运算得到的一样,例如对于(10*9)/(2×1)和(10÷2)×(9÷1),前者将计算90÷2得到45,或者将计算5×9得到49,这两种方法得到的结果相同,但前者的中间值90大于后者,因子越多,中间值的差别就越大,当数字非常大时,这种交替进行乘除运算的策略,可以防止中间结果超过最大的浮点数。

7.3 函数和数组

· 7 min read
CheverJohn

正如标题哈,本篇博文讲的就是结构体和函数。不是新手向的教程哈,更多的这是一篇记录经验的文章。话不多说,开始咯!

我们将注意力从之前的数组转到结构上面。为结构写函数可比为数组写函数要简单得多了。虽然结构变量和数组一样,都可以存储多个数据项。但是在涉及到函数的时候,结构变量的行为更接近与基本的单值变量。这个名词可能太专业了哈。我做一下解释,还是拿数组来做比较,数组中都是一个元素为单位存储的,在结构中,相应的便是将数据组合成一个实体,实体就是数据,数据就是实体。实体(结构)==元素(数组)

这边实现传值的思想主要是利用了一个结构可以赋给另外一个结构这样子的常识知识。就像普通变量一样。我需要额外补充的是,函数将使用原始结构的副本。

函数也可以返回结构 。这边与使用数组名就是代表了数组第一个元素的地址这样的观点不同的是,结构名只是结构的名称,要获得结构的地址,必须使用地址运算符&

咱们传递结构体的基本方法就是按值传递(圈起来,要考!)但是C++作为一门精细的语言,细节到每一个内存都要深挖,不能忍受这种方法的一个缺点:如果结构非常大的话,复制结构将增加内存要求,降低系统运行的速度。出于这种原因,我们更愿意的是采取按地址传递(没错,又是“该死”的指针,所以说C++指针一定要学好!)。然后使用指针来访问结构的内容。

首先介绍“按值传递”

当结构比较小时,按值传递最舒服了用起来。接下来举的例子来源于《C++ Primer Plus(第六版)》

例子:

从a到b城需要3小时50分钟,而从b到g城需要1小时25分钟,对于这种时间,可以使用结构来表示——一个成员表示小时值,另一个成员表示分钟值,将两个时间加起来需要一些技巧,因为可能需要将分钟值转换为小时。例如前面列出的两个时间的总和为4小时75分钟,应将它转化为5小时15分钟,下面开发用于表示时间值的结构,然后再开发一个函数,它接受两个这样的结构为参数并返回表示参数的和的结构。

突出介绍一下结构函数的写法

struct travel_time{
int hours;
int mins;
};

//在已经定义好时间的结构的前提下,开始声明结构函数

travel_time sum(travel_time t1, travel_time t2);

完整代码如下:

#include <iostream>
struct travel_time{
int hours;
int mins;
};
const int Mins_per_hr = 60;

travel_time sum(travel_time t1, travel_time t2);
void show_time(travel_time t);

int main(){
using namespace std;
travel_time day1 = {5, 45};
travel_time day2 = {4, 55};

travel_time trip = sum(day1, day2);
cout << "Two-day total: ";
show_time(trip);

travel_time day3 = {4, 32};
cout << "Three-day total: ";
show_time(sum(trip, day3));

return 0;
}

travel_time sum(travel_time t1, travel_time t2){
travel_time total;

total.mins = (t1.mins + t2.mins) % Mins_per_hr;
total.hours = t1.hours + t2.hours + (t1.mins + t2.mins) / Mins_per_hr;

return total;
}

void show_time(travel_time t){
using namespace std;
cout << t.hours << " hours, " << t.mins << " minutes\n";
}

代码解析

travel_time 就像是一个标准的类型名,可以用来声明变量、函数的返回类型和函数的参数类型。

然后试travel_time结构的变量total 和t1,使用成员运算符进行数据的操作。代码很简单,可以看得懂。

然后介绍“按地址传递”

这一次我换了个题目,题目的内容我简要地说一下哈:通常我们表示一件物品的位置的时候,都是采取选择参照系利用直角坐标系或者极坐标系进行精确表示的,这边写了一个程序用于两种坐标系之间的转换。就是这样。

由于和上面的代码极其类似,所以我认为看看例子就能理解欧,就不做解析了。over

这个代码有一个小东西讲讲熬,就是while里边的cin>> 的用法,cin的特性是可以输入int整型数,但是一旦发现你输入的不是数字的时候,他就会不满足条件,变为0,然后while就会跳出循环,蛮好用的小技巧,推荐学习!

代码奉上:

#include <iostream>
#include <cmath>

struct polar{
double distance;
double angle;
};

struct rect{
double x;
double y;
};

void rect_to_polar(const rect * pxy, polar * pda);
void show_polar(const polar * pda);

int main(){
using namespace std;
rect rplace;
polar pplace;
cout << "Enter the x and y values: ";
while (cin >> rplace.x >> rplace.y){
rect_to_polar(&rplace, &pplace);
show_polar(&pplace);
cout << "Next two numbers (q to quit): ";
}
cout << "Done.\n";
return 0;
}

void rect_to_polar(const rect * pxy, polar * pda){
using namespace std;
pda->distance = sqrt(pxy->x * pxy->x + pxy->y * pxy->y);
pda->angle = atan2(pxy->y, pxy->x);
}
void show_polar(const polar * pda){
using namespace std;
const double Rad_to_deg = 57.29577951;

cout << "distance = " << pda->distance;
cout << ", angle = " << pda->angle * Rad_to_deg;
cout << " degrees\n";
}

· 19 min read
CheverJohn

这是一种基于github托管平台的工具类系统,我们以Travis CI来讲这一项技术的实现。

什么是Continuous Integration(CI)呢?

这是一种在小周期内寻求经常合并代码的一种模式。 与此相对的就是,在一个大的开发周期结束后进行合并的另一种模式。

CI的目标就是以较小的增量开发,一点点的增加自己的代码和经常性的测试,从而构建更加稳定和优秀的软件开发。

​ Travis CI通过自动化生成并且测试代码的改变来实现对我们开发进程的一种帮助。而且如果成功实现了改变,它还能够提供实时的反馈。

​ Travis CI也能在你的程序开发中的其他部分提供帮助,通过管理部署通知

CI的构建以及自动化:Building,Testing,Deploying

​ 当你使用了Build的功能后,Travis CI将会从你的github仓库拉下你的代码,并把它放在一个全新的虚拟环境中。然后执行一系列的build命令进而test你的代码。

​ 如果你的代码test没通过,那么build将会被认为是失败了的;相反如果通过了,build就是成功的,系统便会将代码部署到你想让它部署的地方去。

​ CI build可以自动化你的动作流的其他部分。这就意味着你可以使你的任务和其它的一些部分相互依赖起来,通过Build Stages,设置notifications,准备deployments在你build之后或者更多其他的任务之后的方法。

Builds,Jobs,Stages and Phases

这里是对上面几个名词的解释

  • phase - 这是指一种继往开来的工作模式。举个例子,我们首先要做好deploy部署好阶段,然后我们才关注script阶段,最后我们才能够实现install阶段。
  • job - 一种自动化的进程就是一个job,我举一个例子哈,我们从我们的远程仓库将代码clone下来,clone到一个虚拟环境中去,接下来我们开始执行一系列的阶段比如说编译代码、跑代码测试等等。当然如果我们的job中有一个步骤出现了错误,那么我们的script阶段就会向我们返回一个值来报错。
  • build - 一组jobs。举个例子,一个build命令下可能会有2个jobs,每个jobs都会用各自适合的编程语言来进行测试项目。一个build的完成,也就意味着它的jobs都被成功完成了。
  • stage - 由多个阶段组成的顺序生成过程的一部分并行运行,就称为一组作业

Breaking the Build

当满足一下条件的时候,我们就认为build已经损坏了。(比如说一个或多个作业完成且状态未通过时:

  • errored - 我们在before_install,install, before_script阶段输错了命令导致返回了一串错误代码。这个job就会立刻停止。
  • failed - 在script阶段的一个命令导致返回了错误代码。这个job会继续跑知道它完成了它的工作。
  • canceled - 管理员取消了job

Infrastructure and Environment Notes

对于不同的操作系统,有不同的要求

  • Linux - 以后有用再去官方文档查
  • macos - 同上
  • Windows - 系统版本在1803以上。

Travis CI是在软件开发领域中的一个在线的、分布式的持续集成服务,用来测试在github托管的代码的。好吧上面所有的build可以替换成构建。

开始讲解Travis CI和github page运行机制原理

宝贝们,上面都是我对官方文档的翻译哦,我觉得不是特别妥 现在才是最重要的部分,是精髓哦!我花了(让我算算哈,从早上8:15到现在22:53)14个小时38分钟。是真的“有意思哦”!

闲话少说,开始了

首先得感谢我桃大佬的概念输出,大概是上上周吧,建议我为了我博客的安全着想,将本地的博客配置文件保存到云端,如果本地的硬盘出了啥问题的话,我们也可以从云端恢复数据,完美的主意!桃大佬给了我可持续集成这一概念的输出。

概念输出:什么是可持续集成(CI)呢?

这个网上都能给出正确的解释,这是一种在线托管的持续集成服务,与github是好哥们,当我们用github账号登录了Travis CI(CI的一种)之后,我们就可以看到介家伙上面有所有我们github仓库中的项目。是的呢,就是和github穿一条裤子的呢!我先放图哈!

Travis介绍_一_.jpg

大家可以从图中很容易的看到,我Mr8god在GitHub上的仓库这边都有。概念我也说了,大致样子也给大家看了,我们开始下一步。

Travis CI和GitHub的机制原理

先放图,这是我自己做的图解

Travis CI和GitHub的机制原理.jpg

这一张图可以很好的解释哈,我这边也懒得加水印啥的,想拿就拿。

不管是啥机制,首先的前提是你的思路一定要明确,这也就是为什么网上教程有很多,但是很少有人能够做得出来的原因。大多数人只喜欢找现成的就满足了。但是那些都是治标不治本的玩意儿,真东西还是得往细了研究,以前都觉得那些花里胡哨的东西真好玩,可真等到我玩过了之后才发现,基础才是最重要的东西!!!不禁想到了很多人很早学习了框架……咳咳,不扯远了不扯远不扯远了哈

我先说一下我的持续集成思路

  • 我想到这个的背景:上面也说过了,是我桃大佬提供的(他超棒)。主要是为了解决hexo博客平台无法备份代码源码的问题,因为我如果没有备份的源码,万一我本地的存储系统崩盘了之后,我不就蛋糕了吗?!!!所以现在搭建好这一系统后,如果遇到了本地存储系统崩盘的情况,我们可以从云端(GitHub)上下载代码,实现本地重建。
  • 我的持续集成思路:我的博客源码存储在GitHub上的一个独立的仓库(上图中的GitHubSource)中,博客hexo自动生成的静态文件又是在另外一个独立的仓库(GitHubgitpage)中,每次我在本地把代码推到我的GitHub仓库之后,Travis就会立刻监听到我仓库发生了变动,这个时候Travis就会采取反制措施,具体我后面再细讲。反制措施会生成原本本地就可以生成的博客网站静态文件,并把文件部署到GitHubPage上面。

图解:

Step1:这一步骤,我是将本地的代码推到GitHub上的GitHubSource仓库中去。 Step2:这一步骤得细讲,我们GitHub的好兄弟——Travis在第一时间发现了我存储在GitHubSource仓库里的代码发生了变动(此处就是我Step1中的推代码行为导致的变动,实际生活中也可能是删掉代码之类的其他操作导致的仓库变动),于是它做的第一件事情就是将变动后的代码拉到它自己的虚拟机上,我更愿意将它理解为虚拟机(实际上不是虚拟机哦!)。 Step3:虚拟机接下来会检查拉到本地的源码中是否会存在.travis.yml文件,该文件里边就有指示Travis CI本地虚拟机的命令,下面我详解一下该文件哈!现在我们只要知道该命令可以像本地hexo那样将博客源代码进行编译,生成静态文件。 Step4:接下来我就要向大家补充或者说巩固一个知识点了:hexo博客平台每一次的hexo三连(hexo c && hexo g && hexo d),最后都只是将博客中的public文件部署(上传)到GitHubPage而已。于是咱们的第四步就是将在Travis CI虚拟机中生成的静态文件上传到我的GitHubPage仓库中去。至此,大功告成!

.travis.yml文件详解

先放上我的文件,可以信赖哦!

language: node_js

node_js: stable # 要安装的node版本为稳定版

cache:
directories:
- node_modules # 要缓存的文件夹

install:
- npm install

script:
- hexo clean
- hexo g

after_script:
- cd ./public
- git init
- git config user.name "mr8god"
- git config user.mail "17696748602@163.com"
- git add .
- git commit -m "代码使用Travis CI自动部署的哈"
- git push --force --quiet "https://${blogsource}@${GH_REF}" master:master

branches:
only:
- master # 触发持续集成的分支。现在我们源码在哪里,这边就填哪一个分支,如果该分支发生变化,那么就会启动travisCI


env:
global:
- GH_REF: github.com/Mr8god/Mr8god.github.io.git

参数讲解:

-language:指定Travis CI的基本环境语言,我这边选择的是hexo的基本环境原因——nodejs -node_js:选择安装的node版本,对咯对咯,每一次使用Travis CI的虚拟机都会是一幅全新的模样,该虚拟机每一次的操作都需要安装对应的环境。 -cache:缓存文件存储的位置 -install:这里边我选择了 npm install命令,安装hexo博客平台需要的一些依赖包。 -script:脚本行为开始,可以看到我这边是本地很正常的hexo clean和hexo g命令,不做解释哈。执行完这两个行为后,我们会在虚拟机的public文件夹中生成我们需要的静态网页文件。 -after_script:如其名,执行完脚本后该干什么呢?这边的一串命令是为了将虚拟机的public文件夹中的静态文件上传到GitHubPage仓库中去,实现部署。 -branches:里边填上我们Travis CI所监督的GitHub仓库的分支,只要这个分支出现了代码变动,就会有接下来一波反制措施。 -env:介里边是变量。

综上介绍完了,如果你要用我的配置文件的话,你需要把- git config user.name "mr8god"、 - git config user.mail "17696748602@163.com"、-GH_REF: github.com/Mr8god/Mr8god.github.io.git或者再加上那个分支的内容改一改,改成自己的东西就行了。

实操

这个也是挺重要的!

首先我们在我们的GitHub上建好我用于存放源码的仓库哦!接下来就需要打开GitHub的setting进行GitHub与它好兄弟Travis CI的故事了

GitHub与Travis CI进行token绑定

首先我们打开我们的GitHub主页面,进入我们的setting里,发现了deployer setting 实操(一).jpg

我们点进去,接下来会看到下图 实操(二).jpg

然后继续点我的红圈 实操(三).jpg

然后继续点我的红圈,我们开始创建token了 实操(四).jpg

这边我们要记住我们的note,因为接下来在Travis CI页面我们用得上。

实操(五).jpg

然后这边的这个值也一定要记住!!!这个时候就该把它复制下来了,因为这个页面只会显示一次,如果你错过了,你就得重来亿遍!

实操(六).jpg

接下来,在前一个name的红圈内填上我们的note,后边的value中填上我们刚刚复制的码。就OK了,就实现了这个仓库和我们GitHub仓库的绑定。

【一个常见的错误】:我们在观测代码变动时,在Travis CI的Build History中可能会遇到不管怎么push,依旧 纹丝不动的情况,这个时候就要注意看看自己的.travis.yml文件是否是完整无缺,没有错误的。

在博客文件上我们要做些什么呢?

我们需要在博客文件的根目录下添加我们上面详解的.travis.yml文件,就行了。接下来就是一个容易出错的重点了,而且比较难免描述清楚,我尽力用我的语言描述清楚哈。

如何将本地的hexo博客源码push到GitHub上的source仓库中去

这个地方是困扰我很久的地方,如果这个地方没有做好的话,我们会面临一个直观的问题,我们部署好之后的网站是一片空白,

为什么呢? 因为我们的渲染文件没有部署上去呀!

为什么我们的渲染文件没有部署上去呢? 因为我们源码中的next主题(hexo的一种博客主题)没有push到我们的云端GitHub的source仓库呀!

为什么我们的next主题文件会没有push到我们的云端GitHub上的source仓库中去呢? 这个就要说到我们当初创建本地博客文件的时候了,那个时候我没有注意一个问题,我直接主题源码仓库把代码pull到本地了,这导致了我们的theme——next文件中本身就有一个初始化过、指向原仓库的git了,这构成了git的一种叫做submodule(子模块,就是git文件中还有一个git文件),

这会导致什么结果呢? 会导致本地的next文件夹无法上传到我们的GitHub源代码仓库中去, 进而无法被我们的Travis CI拉到它的虚拟机中去, 进而无法生成CSS、js等网站渲染文件, 进而出现了我们开头所讲到的一片空白现象。

这一点提醒了我,日后在pull别人代码的时候,要做分离处理!!!

那我们该怎么解决呢?

【解决方法】

  1. 首先我们来到我们的next文件夹(博客目录/themes/next)下,删掉.git和.gitnore文件
  2. 然后cd到我们的themes文件夹目录下,清除缓存。命令是:git rm -r --cached next
  3. 然后就是Git三连了(git add . 、 git commit -m "想说啥说啥" 、 git push)
  4. 完美解决问题,别看这么简单,我花了三小时……

大功告成,睡了睡了

有问题联系我江某人哈

QQ:1803357141

img

· 8 min read
CheverJohn

重构前

代码一(Customer)

package cn.mr8god.refactoring;

import java.util.Enumeration;
import java.util.Vector;

/**
* @author Mr8god
* @date 2020/4/12
* @time 00:29
*/
public class Customer {
private String _name;
private Vector _rentals = new Vector();

public Customer(String name){
_name = name;
}

public void addRental(Rental arg){
_rentals.addElement(arg);
}

public String getName(){
return _name;
}

public String statement(){
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()){
double thisAmount = 0;
Rental each = (Rental) rentals.nextElement();

switch (each.getMovie().getPriceCode()){
case Movie.REGULAR:
thisAmount += 2;
if (each.getDaysRented() > 2) {
thisAmount += (each.getDaysRented() -2) * 1.5;
}
break;

case Movie.NEW_RELEASE:
thisAmount += each.getDaysRented() * 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (each.getDaysRented() > 3) {
thisAmount += (each.getDaysRented() - 3) * 1.5;
}
break;
}

frequentRenterPoints ++;

if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) {
frequentRenterPoints ++;
}

result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(thisAmount) + "\n";
totalAmount += thisAmount;
}
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";

return result;

}
}

代码二(Rental)

package cn.mr8god.refactoring;

/**
* @author Mr8god
* @date 2020/4/12
* @time 00:14
*/
public class Rental {
private Movie _movie;
private int _daysRented;

public Rental(Movie movie, int daysRented){
_movie = movie;
_daysRented = daysRented;
}

public int getDaysRented(){
return _daysRented;
}

public Movie getMovie(){
return _movie;
}
}

代码三(Movie)

package cn.mr8god.refactoring;

/**
* @author Mr8god
* @date 2020/4/12
* @time 00:01
*/
public class Movie {

public static final int CHILDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;

private String _title;
private int _priceCode;

public Movie(String title, int priceCode){
_title = title;
_priceCode = priceCode;
}

public int getPriceCode(){
return _priceCode;
}

public void setPriceCode(int arg){
_priceCode = arg;
}

public String getTitle(){
return _title;
}
}

第一次重构后

代码一(Customer)

package cn.mr8god.refactoring;

import java.util.Enumeration;
import java.util.Vector;

/**
* @author Mr8god
* @date 2020/4/12
* @time 00:29
*/
public class Customer {
private static String _name;
private static Vector _rentals = new Vector();

public Customer(String name){
_name = name;
}

public void addRental(Rental arg){
_rentals.addElement(arg);
}

public static String getName(){
return _name;
}

public static String statement(){
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()){
Rental each = (Rental) rentals.nextElement();

result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(each.getCharge()) + "\n";
}
result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
result += "You earned " + String.valueOf(getTotalFrequentRenterPoints()) + " frequent renter points";

return result;

}

private double amountFor(Rental aRental){
return aRental.getCharge();
}

private static double getTotalCharge(){
double result = 0;
Enumeration rentals = _rentals.elements();
while (rentals.hasMoreElements()){
Rental each = (Rental)rentals.nextElement();
result += each.getCharge();
}
return result;
}

private static int getTotalFrequentRenterPoints(){
int result = 0;
Enumeration rentals = _rentals.elements();
while (rentals.hasMoreElements()){
Rental each = (Rental) rentals.nextElement();
result += each.getFrequentRenterPoints();
}
return result;
}
}

代码二(Rental)

package cn.mr8god.refactoring;

/**
* @author Mr8god
* @date 2020/4/12
* @time 00:01
*/
public class Rental {
private Movie _movie;
private int _daysRented;

public Rental(Movie movie, int daysRented){
_movie = movie;
_daysRented = daysRented;
}

public int getDaysRented(){
return _daysRented;
}

public Movie getMovie(){
return _movie;
}

double getCharge(){
double result = 0;
switch (getMovie().getPriceCode()) {
case Movie.REGULAR:
result += 2;
if (getDaysRented() > 2) {
result += (getDaysRented() - 2) * 1.5;
}
break;

case Movie.NEW_RELEASE:
result += getDaysRented() * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (getDaysRented() > 3) {
result += (getDaysRented() - 3) * 1.5;
}
break;
default:
throw new IllegalStateException("Unexpected value: " + getMovie().getPriceCode());
}
return result;
}

int getFrequentRenterPoints(){
if ((getMovie().getPriceCode() == Movie.NEW_RELEASE) && getDaysRented() > 1){
return 2;
}else{
return 1;
}
}
}

代码三(Movie)

package cn.mr8god.refactoring;

/**
* @author Mr8god
* @date 2020/4/12
* @time 00:01
*/
public class Movie {

public static final int CHILDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;

private String _title;
private int _priceCode;

public Movie(String title, int priceCode){
_title = title;
_priceCode = priceCode;
}

public int getPriceCode(){
return _priceCode;
}

public void setPriceCode(int arg){
_priceCode = arg;
}

public String getTitle(){
return _title;
}
}

第一次重构心得

大多数重构都会减少代码量,但这次我们的重构却增加了代码量。

主要原因 这边使用的Java设置了大量语句来实现了一个累加循环。哪怕只是一个简单的累加循环,每个元素只需要一行代码,外围的支持代码也需要六行!虽然这是每个Java程序员都熟悉的写法,但是代码量还是太多了。

与其如此还不如不做这么多操作呢。再来说一下代码性能问题。原本代码只需要执行while循环一次就行了,经过我第一次重构后,代码居然要执行三次。进而代码的耗时可能就会很多,就可能大大降低了程序的性能。但是呢,这些本应不是我重构需要关心的问题,因为我其实本不知道我的代码经过一次重构后的性能会如何(害,说到这个我就想起了我的JProfiler,我现在还没能掌握它,难受),这需要专业的代码性能工具来进行测试。再说了,关于代码的性能,我们可以到后期进行性能优化时再考虑嘛。所以还没到最后呢!我们没有必要过早作出判决呀!

那么现在我们说一下优点哈。Now,Customer类内的所有代码都可以调用这些查询函数(就是我之前将临时变量浓缩成的那几个函数)。如果系统其它部分需要这部分信息,也可以轻松地将查询函数加入Customer类接口中。如果没有这些查询函数,其他函数就必须了解Rental类,并自行建立循环,这势必会增大程序的编写难度和维护难度。从而提高代码出错的概率。

好了好了,我们不能仅限于此哈,现在假设我们的产品经理又要跟我们提需求了,他们想让我们增加一个修改影片分类规则的功能。但是呢有没有具体告诉我们这究竟是一个怎样的分类规则。我们尚未清楚他们想怎么做,只知道新的分类法很快就要引入了。现有的分类法马上就要变了。与之相应的部分就是我们的计算方式和常客积分计算。我们现在必须要进入我们的计算方法和常客积分中,把因条件而异的代码替换掉,这样才能为将来的改变换上一层保护膜。我们开始新一轮的重构了!