黑马程序员技术交流社区
标题:
【石家庄校区】笔记总结
[打印本页]
作者:
学习让我快乐啊
时间:
2018-4-14 17:43
标题:
【石家庄校区】笔记总结
本帖最后由 小石姐姐 于 2018-4-20 11:27 编辑
【石家庄校区】笔记总结
d
ay5:
Object的toString()方法
是类层次结构的根类(上帝)
所有类都直接或间接继承了 Object 类
举例:
class Student {} : Student默认继承了Object类
class Student extends Person {} : Student继承Person类, Person类继承Object类
成员方法
String toString() : 返回对象的字符串表示方式
重写toString()的作用: 任何类可以重写toString()方法来自定义输出内容. 开发调试时经常用, 主要是查看对象的属性值
实现方式: return getClass().getName() + "@" + Integer.toHexString(hashCode());
获取字节码对象的3种方式
Object类的 Class getClass() 方法
示例: Class clazz = student.getClass();
2. 类名.class 属性
示例: Class clazz = Student.class;
扩展: 实际上 .class 是一种特殊形式, 称为 类字面常量 , class是关键字不能作为属性名, 且Object类中并没有定义这个属性
3. Class类的静态方法 forName(String className)
示例: Class clazz = Class.forName("com.itheima.Student");
注意: 因为该方法使用字符串来寻找类, 有可能并没有该字符串对应的类, 所以会抛出
ClassNotFoundException 类无法找到的异常
类的字节码对象注意点:
同一个类的字节码对象只有一个, 地址值是相同的, 无论创建了多少个对象
Class 和 class 区别?
Class是一个类, 表示字节码文件的类
class是一个关键字, 用于定义一个类
Object类的equals方法
Object类
boolean equals(Object obj) : 比较两个对象是否"相等
String类和Object类的 boolean equals(Object obj) 区别
String类的equals: 比较字符串内容是否相同. 重写了Object类的equals方法
Object类的equals: 使用 == 来比较, 基本数据类型比较值, 引用数据类型比较对象的地址值
System类概述
java.lang 包下的类, 不用导包就可以用
System 类: 包含一些有用的类字段和方法, 不能被实例化
类字段: 静态成员
err : 标准错误输出流
in : 标准输入流
out : 标准输出流
static long currentTimeMillis() : 返回当前系统时间的毫秒值, 从零时区1970-01-01 00:00:00
开始算
static void exit(int status) : 终止虚拟机的运行, 0表示正常退出, 非0表示异常退出
System类的静态方法: arrayCopy()
static void arrayCopy(Object src, int srcPos, Object dest, int destPos, int length) : 复制源数组
中指定长度个元素到一个新数组中
src: 源数组
srcPos: 指定从源数组的哪个索引开始复制
dest: 目标数组
destPos: 指定目标数组接收元素的索引位置
length: 目标数组接收元素的个数
注意: 复制时注意数组长度, 不要超出索引, 否则会出现数组越界异常
Date类
注意:
有2个包中都有Date类, 一个是 java.sql.Date , 另一个是 java.util.Date , 我们用的是
java.util.Date
Date类: 日期类, 表示特定的瞬间, 可以通过方法来设定所表示的时间, 可以表示任意的时间
构造方法
Date Date() : 创建Date对象, 并表示当前系统时间
Date Date(long date) : 创建Date对象, 使用指定的时间
成员方法
String toLocalString() : 转换为本地时间格式的字符串
SimpleDateFormat SimpleDateFormat(String pattern) : 创建对象同时指定格式
成员方法
String format(Date date) : 将Date对象格式化为字符串
Date parse(String source) : 将字符串解析为Date对象
常用日期模式:
y : 年
M : 月
m : 分
D : 年中的天
d : 月中的天
H : 小时(24小时制)
K : 小时(12小时制)
S : 毫秒
s : 秒
E : 星期
示例: yyyy-MM-dd E HH:mm:ss.SSS , 结果为 2016-04-01 星期五 17:29:15.868
注意:
每个 SimpleDateFormat 对象都有一个对应的模式, 无论 format() 还是 parse() , 都是用该对象的模式
操作
如: new SimpleDateFormat("yyyy-MM-dd") 和 new SimpleDateFormat("yyyy年MM月dd日") 两个对象的模
式就不一样
Calendar的概述
Calendar 类: 日历类, 也是处理时间和日期的类, 用于替代Date类
创建对象
静态方法: static Calendar getInstance()
示例: Calendar c = Calendar.getInstance();
静态字段: 表示时间的某个部分
static YEAR : 年份
static MONTH : 月份. 注意月份数值是0-11
static DAY_OF_MONTH : 日期
static HOUR : 小时(12小时制)
static HOUR_OF_DAY : 小时(24小时制)
static MINITE : 分钟
static SECOND : 秒
包装类
包装类是封装了基本数据类型的类, 提供了更多复杂的方法和变量
同时将基本类型的使用转换为了类的面向对象方式
构造方法
Integer(int value) : int转Integer
Integer(String value) : String转Integer
基本数据类型和引用数据类型的对应关系
byte: Byte
short: Short
char: Character
int: Integer
long: Long
float: Float
double: Double
boolean: Boolean
Integer类的转换方法
int转Integer: Integer Integer(int value)
String转Integer: Integer Integer(String s)
Integer转int:
int intValue() : 将Integer对象的值转换为int类型
String转int:
static int parseInt(String s) : 将字符串转换为int
static Integer valueOf(String s)
int转String
"" + int
Integer对象的 toString() 方法
static String toString(int i) : 将int转换为String
包装类的自动装箱和自动拆箱
箱=包装
什么是自动装箱?
概念: 基本数据类型可以自动转换为包装类
示例: Integer i = 10;
原理: 相当于执行 Integer i = new Integer(10);
或 Integer i = Integer.valueOf(10);
什么是自动拆箱?
概念: 包装类可以自动转换为基本数据类型
示例: Integer i = 10; int a = i;
原理: 相当于执行 int a = i.intValue();
正则表达式概述
正则表达式是一种用于匹配字符串的规则
可用于判断字符串是否符合某种规则, 比如是否是邮箱, 是否是手机号等等
Pattern 类:
创建正则表达式模板, 内有正则表达式规则
字符:
x : 一个字符x a b c "abc" "aab"
\\ : 表示 \ 反斜杠.
为什么有2个? 第一个 \ 是转义的作用
[abc] : a或b或c, 即中括号字母中的其中一个 [abc]{2} "aa"
[^abc] : 除a, b, c之外的任何字符.
^ 表示否定
[a-zA-Z] : 小写英文a到z, 以及大写英文A到Z.
- 表示范围
什么叫转义?
就是转换含义. 因为正则中有一些特殊字符, 如 + , - , * , \ 等都有特殊意义, 所以在作为普通字符使用
时必须转义, 如:
用 \\ 转义: 因为 \x 可能表示某种符号, 如 \t 所以要用2个反斜杠
+ 表示一个或多个, \\+ 表示加号字符
- 表示范围, \\- 表示减号字符
* 表示零个或多个, \\* 表示星号字符
用 \ 转义:
\ 要结合某些字符进行匹配, \\ 表示斜杠字符
切割字符串
String类
String[] split(String regex) : 按照正则表达式切分当前字符串为多个部分, 返回每个部分组成的字符串数组, 其中不包含分隔符
预定义字符
. : 任何字符(对于行结束符, 可能匹配, 也可能不匹配)
\d : 数字0-9. 相当于 [0-9]
d是digital的缩写
\D : 非数字. 相当于 [^0-9]
\s : 空白字符. 相当于 [ \t\n\x0b\f\r] , 包括空格(第一个空就是空格)
s是space的缩写
\S : 非空白字符. 相当于 [^\s]
\w : 单词字符. 相当于 [a-zA-Z_0-9]
w是word的缩写
\W : 非单词字符. 相当于 [^\w]
Greedy数量词
X? : X这个字符出现一次或零次
X* : X这个字符出现零次或多次, 包括一次
X+ : X这个字符出现一次或多次
X{n} : X这个字符恰好出现n次
X{n, } : X这个字符至少出现n次
X{n, m} : X这个字符至少出现n次, 但不超过m次
Day6:
集合的体系结构
集合:
是一个统称, 包含很多集合的实现类. 根据不同数据结构, 提供了不同的集合类
数据结构:
Collection 接口
是单列集合体系的顶层
本身是接口, 实际使用的是子类对象. 使用多态, Collection c = new ArrayList<>();
常用方法
增
boolean add(E e) : 向集合中添加元素, 添加成功返回true, 添加失败返回false. 永远能够添加
成功
删
boolean remove(Object o) : 删除元素中的指定元素, 删除成功返回true, 删除失败返回false
void clear() : 清空集合中的元素
判断
boolean contains(Object o) : 判断集合中是否包含指定元素, 包含返回true, 否则false
boolean isEmpty() : 判断集合是否为空集合, 即没有元素. 没有元素返回true, 否则false
获取个数
int size() : 获取集合中元素的个数
转换
Object[] toArray() : 将集合转换为Object[]
迭代器
Iterator 接口:
迭代器
作用:
提供遍历集合的安全方式
获取迭代器:
使用集合对象调用 Interator<E> iterator() 方法
成员方法
boolean hasNext() : 是否有下一个元素
E next() : 返回下一个元素
void remove() : 从迭代器指向的 collection 中移除迭代器返回的最后一个元素
并发修改异常:
ConcurrentModificationException
并发:
并行发生, 即同时发生
修改:
指的是会改变集合长度的操作
增加元素
删除元素
清空元素
list的 set(int index, Object o) 方法可以修改集合中某个索引处的元素值, 但并不会改变集
合的长度, 所以不会发生并发修改异常
发生原因:
迭代器依赖于集合, 相当于集合的一个副本, 当迭代器在操作时, 如果发现迭代器的副本和集合不一
如何避免:
// 添加元素
collection.add("Hello");
collection.add("world");
collection.add("java");
// 方式1: 转换为Object[]
Object[] arr = collection.toArray();
for (int i = 0; i < arr.length; i++) {
System.out.println(arr
);
}
System.out.println("-------------");
// 方式2: 使用迭代器
// 先通过集合对象获取迭代器对象
Iterator iterator = collection.iterator();
// 循环迭代
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
方式1: 不使用迭代器
方式2: 在使用迭代器遍历时, 使用迭代器的方法修改
添加元素:
List接口中的 ListIterator listIterator() 获取用于List的迭代器, 然后调用
ListIterator的add()方法
删除元素: remove()
List接口
列表, 是Collection的子接口
特点:
元素存取有序, 元素可以重复, 有索引
泛型
Generic type, 广泛的通用的类型, 具体什么类型由开发者定义
好处:
避免类型转换问题
减少警告
简化代码书写
作用:
用于明确数据类型
将运行时才发生的类型转换问题, 提前到了编译时期
如何使用:
出现 <某个字母> 的地方, 就可以使用具体的数据类型替代这个字母
foreach的概述(增强for循环)
foreach:
增强for循环
作用:
遍历数组和集合
格式
// foreach格式
for (int i = 0; i<arr.length;i++) {
int a = arr
;
}
for (元素类型 变量名: 数组或集合对象) {
// 每次循环都会遍历出一个元素, 保存到变量名中
}
int[] arr = {1,2,3};
for (int i : arr) {
Syso.out.println(i);
}
注意:
增强for循环中不能修改集合(改变集合长度), 会发生 并发修改异常
原因: 因为增强for循环内部使用的是迭代器进行迭代
增强for的优缺点:
优点: 简单快捷的拿到每一个元素
缺点:
循环过程中不能修改集合
不能像普通for循环那样使用索引
}
常见数据结构: 数组
数组的特点:
长度一旦确定则不可改变
元素有整数索引
只能存储同一类型的元素
既可以存储基本数据类型, 也可存储引用数据类型
数组的增删改查:
增加/插入元素:
创建一个新数组, 长度为原数组长度+1
遍历原数组, 将原数组的元素复制到新数组的相同索引位置, 直到遇到要增加元素的索引位置
将要增加的元素值赋值到索引位置
继续复制剩余元素
删除元素:
创建一个新数组, 长度为原数组长度-1
遍历原数组, 将原数组的元素复制到新数组的相同索引位置, 直到遇到要删除元素的索引位置
跳过要删除的元素, 继续将剩余元素复制到后续索引位置
修改元素: arr[0] = 10;
获取元素: int i = arr[0];
通过以上增删改查的操作, 总结出数组的特点:
增删慢
查询快
链表
链接列表, 好比通过链子链接起来的一些结点
链表的增删改查
增加元素:
修改下一个结点的地址值
删除元素:
将要删除结点的前一个结点中保存的下一个结点地址值, 修改为要删除结点的下一个结点的地址值
(也就是说, 直接跳过要删除的元素)
修改元素:
从第一个结点根据其下一个结点地址值不断去寻找, 然后修改值
获取元素:
从第一个结点根据其下一个结点地址值不断去寻找
栈,队列
栈(stack)
特点:
先进后出(FILO, First In Last Out). 或后进先出(LIFO, Last In First Out)
便于理解:
像手枪子弹匣, 入口和出口是一起的
队列(queue)
特点:
先进先出(FIFO, First In First Out).
便于理解:
像管道, 一头入口, 另一头出口
List 接口
继承自 Collection 接口. 在 java.util 包下
特点
元素有序(存入和取出的顺序一样)
有整数的索引
元素可以重复
特有功能 (都是按索引进行操作的)
void add(int index, E element) : 添加元素
E remove(int index) : 删除元素
E set(int index, E element) : 修改元素
E get(int index) : 获取元素
List的子类概述, LinkedList特有功能
List的子类
ArrayList : 底层数组结构. 查询快, 增删慢
常用方法之前学过
LinkedList : 底层链表结构. 查询慢, 增删快
特有方法 (用于处理开头和结尾的元素)
void addFirst(E e) : 将元素添加到开头
void addLast(E e) : 将元素添加到末尾
E getFirst() : 获取开头的元素
E getLast() : 获取结尾的元素
E removeFirst() : 删除第一个元素, 并返回删除的元素
E removeLast() : 删除最后一个元素, 并返回删除的元素
Day7:
Set体系
Set接口:
继承自Collection接口
注意:
无序: 指的是存入的顺序和取出的顺序不一样, 而不是说每次取出来是随机顺序, 存入后每次取出来的顺序
都一样, 但和存入的顺序不一样
直接打印和获取元素都属于取出, 所以不要把打印当成是HashSet内部存储的样子
HashSet类: 是Set接口的实现类
成员方法
boolean add(E e) : 添加成功true, 添加失败(重复了)返回false
HashSet保证元素唯一性的原理
判断对象是否相同要根据2个方法:
1. hashCode()
如果判定哈希值不相同, 则认为不重复
如果判定哈希值相同, 则需要继续使用 equals() 方法判断
2. equals()
如果判定不相同, 则认为不重复
如果判定相同, 则认为重复
而学生对象中的hash值是不同的, 所以认为不重复
HashSet且去重原理(简单版)
重写hashCode()和equals()方法让HashSet可以去掉属性重复的
学生
去重的2个判断方法
先调用元素的hashCode()方法判断哈希值是否相同
如果相同, 再调用元素的equals()方法判断
注意:
重写的是元素的类中的hashCode()和equals()方法, 因为比较的是元素
5分钟练习: 重写学生类的hashCode()和equals()方法
继续使用项目s2-day07, 继续使用包com.itheima.practice_01
修改Student类
重写hashCode()方法, 让其永远返回1
重写equals()方法
再次运行, 查看是否去重
}
}
* HashSet的add()方法, 首先将新添加的元素和当前集合中的已有元素进行hash值比较, 依靠对象的"hashCode()"
* 如果hash值不一样, 则认为不重复, 直接添加新元素
* 如果hash值一样, 则继续与已有元素比较"地址值"或使用"equals()方法"进行比较
* 如果比较结果一样, 则认为重复, 不添加
* 如果和所有的已有元素比较都不一样, 则认为不重复, 添加元素
所以, 自定义对象的hashCode()和equals()方法决定了该对象能否被去重
hashCode()方法和equals()方法的优化
优化hashCode()方法, 让该方法的返回值和成员属性相关联, 减少equals()的调用, 提高效率
方案:
让hashCode()方法返回所有成员变量之和.
基本数据类型直接相加
引用数据类型获取该成员变量的hashCode方法返回值后再相加(boolean不能参与运算)
优化equals方法
提高效率: this == obj return true
提高向下转型的安全性: this.getClass() == obj.getClass() return false
Collections工具类
Collection 和 Collections 有什么区别?
Collection是接口, 是单列集合的顶层, 包含一些共性方法
Collections是类, 提供操作Collection的一些工具方法, 类似的还有操作数组的Arrays工具类
Map接口的概述
Map<K, V> 接口
是双列集合的顶层, 和Collection属于同级
特点
存储方式是 key-value (键值对)方式, 即一个键对应一个值
一个键只能映射一个值
键不能重复, 值可以重复
键是无序的
Map和Collection的区别
Map是双列集合, 用于处理有 一一对应 关系的数据,键不能重复且键无序
Collection是单列集合, 有不同的子体系, List允许重复且有序, Set不允许重复且无序
HashMap类
多态创建对象: (也可以不使用多态)
Map<K, V> map = new HashMap<>();
常用方法:
增和改
V put(K key, V value) : 添加键值对.
注意: 如果key已经存在, 则会使用新的value覆盖原有value, 并将原有value返回
删
void clear() : 清空集合
}
V remove(Object key) : 删除指定键的值(key和value都会删除)
查
V get(Object key) : 通过指定键获取值
int size() : 获取集合长度
判断
boolean containsKey(Object key) : 是否包含指定的键
boolean containsValue(Object value) : 是否包含指定的值
boolean isEmpty() : 是否为空
遍历
Set<Map.Entry<K, V>> entrySet() : 获取键值对的Set集合
Set<K> keySet() : 获取所有键的Set集合
Collection<V> values() : 获取所有值得Collection集合
可变参数
从JDK1.5开始
可变参数: 方法的形参可以传入不定数量个参数
定义格式: 形参数据类型... 形参名
注意事项:
可变参数的数量可以从0个到多个
可变参数只能是同一种数据类型
可变参数的定义位置必须在方法形参列表的最后一个
可变参数的原理:
底层是一个根据形参数量创建长度的数组, 通过JVM将形参转换为数组
如何使用可变参数?
在方法中使用: 把可变参数的形参名当做一个数组变量名, 遍历并获取元素
想方法中传入参数: 将多个参数值使用逗号分隔传入方法
Map嵌套演示: Map嵌套Map
Map<String, Map<String,String>>
理清key和value
存入: 先处理好内部的Map, 然后再放入外部Map
取出: 遇到Map就使用2种方式遍历即可
案例关系图解
Map<String, Map<String,String>>
Map(itheima)
key value
|- "基础班"=Map(base)
key value
|- "01"="zhangsan"
|- "02"="lisi"
|- "就业班"=Map(job)
key value
|- "03"="wangwu"
|- "04"="zhaoliu"
Map嵌套演示: Map嵌套Collection
Map<String,Collection<Student>>
理清key和value
存入: 先处理好内部的Collection, 然后再放入外部Map
取出: 遇到Map就使用2种方式遍历, 遇到Collection就用3种方式遍历
案例关系图解
Map<String,Collection<Student>>
Map(itheima)
key value
|- "基础班"=ArrayList(base)
|- Student("01", "zhangsan")
|- Student("02", "lisi")
|- "就业班"=ArrayList(job)
|- Student("03", "wangwu")
|- Student("04", "zhaoliu")
Day8:
异常:
就是不正常的情况, 是代码编译或运行时发生的错误
常见异常
NullPointerException
ArrayIndexOutOfBoundException
任何数字(负数, 0, 正数)除以0都会抛出异常
异常体系结构
异常输出信息详解
异常的处理方式: JVM默认处理方式, try...catch方式
异常的处理方式
java.lang.Throwable(最顶层)
|_ Error (不应该试图捕获的严重问题, 不能处理的异常)
|_ Exception (可以处理的异常) # 编译时异常: 编译时期就会发生的异常
|_ RuntimeException # 运行时异常: 编译时正常, 运行时才会发生的异常
异常的处理方式
JVM默认处理方式
如果开发者没有处理异常, 则JVM会把异常的类型, 原因, 位置显示在命令行上, 并终止程序. 发生异常
之后的代码都不会再执行
手动处理异常的2种方式
1. 捕获异常并处理: try...catch...
提前对可能发生的异常进行处理, 纠正错误并避免程序终止.
2. 直接抛出异常: 在方法声明上添加 throws 异常类型
不捕获异常, 当发生异常时向方法外界抛出, 直到最顶层的方法, 最终终止程序
一般在无法处理异常, 或希望外界处理异常时使用
处理方式举例:
try catch:
方法内部就捕获了: 买了有问题的产品, 用户找到公司售后, 售后直接处理并退了款
throws:
先抛出去, 让调用者处理: 买了有问题的产品, 用户找到公司售后, 售后说解决不了, 找经理解决, 经理
同意退款解决
一直向上抛, 直到main方法: 买了有问题的产品, 用户找到公司售后, 售后解决不了, 找经理, 经理解
决不了, 找老板, 老板也解决不了. 用户把公司砸了(谁也别干了, 程序停止
使用 try...catch... 处理异常的代码格式:
格式如何记忆?
try是尝试的意思, catch是抓住的意思. 可以理解为尝试运行可能会发生异常的代码, 一旦发生异常, 则抓
住这个异常, 解剖他的信息, 不让程序终止
try...catch 的执行顺序:
先执行try代码块中的代码
如果没有发生异常, 则不执行catch代码块的代码, 继续执行catch代码块之后的代码
如果发生了异常, 则从try代码块中发生异常的那一行代码直接进入catch代码块中执行, 然后继续执
行catch代码块之后的代码
直接进入就是说, try代码块中发生异常那一行代码之后的代码不会执行
注意:
catch可以捕获到该异常类型及其子类的异常, 这是多态
catch括号中应该填什么异常呢?
如果是编译时异常, IDE会提示, 直接使用
2. 如果是运行时异常, 只能运行一次该代码, 看到异常信息的类型后, 手动复制过来. 以后记住了再直接写
3. 直接使用Exception类型, 它包含了所有异常的子类(多态)
异常的处理方式: 在方法声明上抛出
声明方法会抛出异常
使用 throws 关键字(记住这个带s)
定义位置: 方法形参后, 如果抛出多个异常, 可以用 , 逗号分隔
作用: 不捕获异常, 当发生异常时向方法外界抛出, 直到最顶层的方法, 最终终止程序
使用场景: 一般在该方法不能处理异常, 或希望外界处理异常时使用
多异常的捕获:
可以写多个catch代码块来分别处理同一个try代码块中的多个异常
多异常捕获中catch的顺序
如果被捕获的异常类没有继承关系, 则catch的顺序可以随意定义
如果异常是继承关系的类, 必须子类异常在上, 父类异常在下
原因: 因为父类包含子类的异常, 如果上方被父类异常的catch捕获了, 那么捕获子类异常的catch永
远也不会用到
不同异常类型之间用 | 分隔, 变量名只有一个
catch代码块中, 由于不同异常类型都只对应同一个变量名, 所以在需要判断是哪种异常时, 可以使
用 变量名 instanceof 异常类型 来判断具体是何种类型; 如果不需要针对具体异常类型分别处理, 可
以不判断异常类型, 统一通过异常变量名来做统一的处理
Throwable类
常用成员方法
String getMessage() : 异常的信息. 没有原因返回 null
String toString() : 异常的类型和原因
void printStackTrace() : 使用标准错误输出流打印异常信息
finally关键字
作用:
与try...catch配合, 无论异常是否被捕获, 最终都会执行finally代码块中的代码
应用场景:
如IO流的关流操作, 无论IO操作过程中是否出现异常, 最终都要关流, 避免程序占用资源. 所以一般都
在finally代码块中进行关流操作
异常的分类
编译时异常: 编译时就发生的异常
Exception 及其子类(除RuntimeException体系)
处理方式: 编译时期必须处理(try...catch或throws)
运行时异常: 编译时正确, 但运行时发生的异常
RuntimeException 类及其子类
处理方式: 编译时期可以自由选择处理或不处理
自定义异常
自定义编译时异常类:
定义一个类, 继承 Exception 或其子类(非RuntimeException), 重写构造方法
自定义运行时异常类:
定义一个类, 继承 RuntimeException 或其子类, 重写构造方法
使用自定义异常的方式:
使用编译时异常:
在方法声明上, 使用 throws 抛出 Exception 或其子类的类型(除RuntimeException体系)
使用运行时异常:
在方法内部的代码中, 使用 throw (注意没有s)抛出 RuntimeException 或其子类的对象
递归
递归思想: 将大问题拆为小问题, 将小问题继续拆为小问题, 所有的小问题解决了, 大问题也就解决了
作用: 简化代码书写
注意事项:
必须要定义方法的结束方式(出口). 无法结束的方法会导致内存溢出
递归次数要注意. 递归次数太多会导致内存溢出
欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/)
黑马程序员IT技术论坛 X3.2