题记:

花了一周把Peter Haggar的《practical Java》看了遍,有所感悟,年纪大了,

写下笔记,方便日后查看.也希望有缘之人可以看看,做个渺小的指路人。

不足之处还望指正。

概述:

全书分为六个部分,包括一般技术、对象与相等性、异常处理、性能、多线程、对象。

一般技术:举例了几个java常见错误用法的说明和解释,诸如array和vector的选择,多态与instanceof等等

对象和相等性则:针对equals的详细说明,是迄今本人见过对equals理解最深的一本书了,其中不乏java的一些规范

异常处理:主要介绍了java异常机制的使用细节,其中有一点就是return后的逻辑一律不执行在try finally模式里头是无效的

性能:介绍了java常用的一些优化细节,诸如使用栈变量来代替堆变量,减少同步化,使用arraycopy方法来代替自己的循环复制数组等等

多线程:简要的说明了java线程使用中一些常见的知识点,如果对java多线程有兴趣的,可以看看《Java并发编程实战》

对象:介绍了接口与继承的关系与使用,对深入学习java框架源码有一定的帮助,感兴趣的可以多思考下其中的奥义

正题:

正题中将挑选自己觉得比较有用的一些知识点进行说明,但并不代表其他知识点就不重要(因人而异,尽信书 不如无书)

最后我会将代码工程打包好,感兴趣的可以去下载下来看看。

1.一般技术

实践1:参数以by value 方式而非by reference 方式传递

 /**
* java有值(基础类型变量value)传递和引用(reference)传递
* 两者的区别是引用将会随着调用方法体的逻辑而发生改变
* 一个非常基础的java知识点
* 献给千千万万徘徊在java门口的求学者
* @author lwx
* TODO
* 参考:
* 2014-5-13 上午9:28:38
*/
public class Lesson1 { public static void valueTest(int value){ value=value+5;
System.out.println("valueTest-->"+value);
} public static void referenceTest(StringBuffer obj){ obj.append("123"); System.out.println("referenceTest-->"+obj.toString());
} public static void main(String[] args) {
int value=2; valueTest(value);
System.out.println(value);//
StringBuffer obj =new StringBuffer("0");
referenceTest(obj);
System.out.println(obj.toString());// }
}

实践2、3:final的用法 略

实践4:在arrays和vectors之间慎重选择

数组在java中使用率远远超过vector,因此这里就要明白何时应该使用vector

首先vector是线程安全的,这样就保证了他可以同步控制对象,

其次vector内部实现也是数组,只不过是泛型对象的数组(数组更多时候存储的是基础类型)

最后就是数组的大小是无法自行拓展的,而vector是可以通过System.arraycopy()方法进行复制扩展vector的容量

实践5:多态优于instanceof

这里必须明白java多态和instanceof用法,简单说下两者的概念

所谓的多态就是不同对象对同一个消息作出不同的响应,java中的多态包含了重载和重写(覆盖)

instanceof 运算符是用来在运行时指出对象是否是特定类的一个实例,通过返回一个布尔值来指出,这个对象是否是这个特定类或者是它的子类的一个

先看下面一个例子

 /**
* 尽量使用多态而非instanceof
* @author lwx
* TODO
* 参考:
* 2014-5-13 上午10:39:41
*/
public class Lesson5 {
public static void main(String[] args) {
Employee mgr=new Manager();
Employee pgr=new Programmer();
System.out.println("经理的工资-->"+calcSalary(mgr));
System.out.println("程序员的工资-->"+calcSalary(pgr));
} public static int calcSalary(Employee e){
int salary=e.salary();
if(e instanceof Programmer)
salary+=((Programmer)e).bonus();
return salary;
}
}
interface Employee{
public int salary();
}
class Manager implements Employee{
private static final int mgrSal=10000;
@Override
public int salary() {
// TODO Auto-generated method stub
return mgrSal;
}
}
class Programmer implements Employee{
private static final int pgrSal=6500;
private static final int pgrBonus=1000;
@Override
public int salary() {
// TODO Auto-generated method stub
return pgrSal;
}
//程序员除了工资 还有项目奖金哦 那个公司有 求收留
public int bonus() {
// TODO Auto-generated method stub
return pgrBonus;
}
}

上面例子中 程序员在计算工资的时候是需要考虑奖金的,因此通过Instanceof来判定传给

calcSalary方法的参数是否是Programmer类,如果是 则在原有工资计算方法上加上bonus()方法
表面上看,这样的逻辑没有问题,但是我们是需要考虑拓展的,加入现在还有产品经理,他的工资也有奖金 另外还有其他福利,在加上其他岗位 那么每增加一个岗位的变动
我们都需要去修改calcSalary方法,而这样的设计明显是不符合java的规范的
书本作者给出的方案是让经理也有奖金的方法,这不过这个奖金是0 从而避免了instanceof的产生,具体做法看书本,此处略
实践6:必要时才需要instanceof
java支持父类向下转型,即使是错误的向下转型 在编译的时候是不会报错的,因此容易让开发人员带来干扰

 /**
* 必要时才用instanceof
* 必要的时候指的是 你需要父类向下转子类
* @author lwx
* TODO
* 参考:
* 2014-5-13 上午11:18:32
*/
public class Lesson6 { public static void main(String[] args) { Shape circle=new Lesson6.Circle();
Object triangle=new Lesson6.Triangle(); //Lesson6.Triangle tri1=(Lesson6.Triangle )circle;//编译通过 但是执行会报错 java.lang.ClassCastException
if(circle instanceof Lesson6.Triangle){ Lesson6.Triangle tri1=(Lesson6.Triangle )circle;
}
Lesson6.Triangle tri2=(Lesson6.Triangle )triangle;
} static class Shape{}
static class Circle extends Shape{}
static class Triangle extends Shape{} }

实践7:一旦不再需要object reference,就将它设为null

接触java的都明白java自带的虚拟机有垃圾回收机制,不愿太操心内存问题,其实作为一名合格的javaer也是需要考虑内存泄露的

况且java确实有存在,当然这里不再我们的讨论话题中,为什么没用的引用尽量要手动的去触发unusefulObj=null呢

其实就是减轻JVM的工作量,gc不是随时触发的 这个应该要懂得

实例中的例子已经很不错了

 /**
* 手动去设置无用的引用为null
* @author lwx
* TODO
* 参考:
* 2014-5-13 下午1:56:03
*/
public class Lesson7 { public static void main(String[] args) { //testGC();//GC测试CPU性能 Customers customers=new Customers(""); //执行一堆逻辑 此处省略 /*
1.
无用时候释放对象
customers=null;//
*/ /*2.
* 上面的情况 存在一个问题 假如customers我们还需要
* 只是他的数据我们不需要引用了 或则customers生命周期跟系统应用一个周期
* 那么我们就只需要释放custIdArray内存就可以达到效果了
* */
//由于我们没有办法直接接触 因此需要开放方法给我们去触发 加入一个unrefCust()方法
customers.unrefCust();// } private static void testGC() {
Runtime rt=Runtime.getRuntime();
long mem=rt.freeMemory();
System.out.println("空闲CUP==>"+mem/1024/1024);
System.gc();//手动去触发虚拟机回收垃圾
mem=rt.freeMemory();
System.out.println("忙时CUP==>"+mem/1024/1024);
} } class Customers{ private int []custIdArray; public Customers(String db){
int num=queryDB(db);
custIdArray=new int[num];
for (int i = 0; i < num; i++) {
custIdArray[i]=i;
}
}
int queryDB(String sql){ return 5;
} public void unrefCust(){ custIdArray=null;
} }

2.对象与相等性

实践8:区别reference类别和primitive型别

这个可能比较拗口很难理解字面的意思,其实也是实践1所说的基础类型和对象引用之间的区别

这里主要是介绍下java1.5的一个新特性:拆箱和装箱

Integer i1=100;//等同于new Integer(100)  这里就是一个自动装箱的过程

int k=i1;// 自动拆箱的过程

更多关于拆箱与装箱 可以移步http://www.cnblogs.com/danne823/archive/2011/04/22/2025332.html

关于包装类的缓存:http://blog.csdn.net/yaoweijq/article/details/6021706

/**
* 基础类型和引用类型对象区别
* 1.5新特性 装箱和拆箱
* @author lwx
* TODO
* 参考:
* 2014-5-13 下午2:15:45
*/
public class Lesson8 { public static void main(String[] args) { //八大基础类型和对应的装箱类
/* boolean char byte short int float long double 对应的对象为 Boolean Character Byte Short Integer Float Long Double*/ Integer i1=100;//等同于new Integer(100)
Integer i2=100;
System.out.println(i1==i2);//true
i1=1000;
i2=1000;
System.out.println(i1==i2);//false } }

实践9:区分==和equals

这个是java经常碰到的一个基础知识点,即"=="和"equals"区别,何时使用==何时使用equals

总结起来可以这么说:== 对于基础类型 比较的是vlaue,而引用类型比较的是地址,当对象不需要单纯的比较地址

而需要你自己DIY的时候,请重写equals方法吧

至于何时使用,可以这么说:==经常是基础类型在用,引用类型的基本不用

equals最常见,而且多数情况下你是需要重写的

有点以偏概全,希望拍砖

实践10:不要依赖equals()的缺省实现

不啰嗦了,直接上代码

 /**
*
* 重写父类equals方法(默认重写的是Object的equals方法)
* @author lwx
* TODO
* 参考:
* 2014-5-13 下午3:46:15
*/
public class Lesson10 { public static void main(String[] args) { BasketBall b1=new BasketBall("brand",20.0);
BasketBall b2=new BasketBall("brand",20.0);
System.out.println(b1.equals(b2));//不重写 则调用Object equals的方法
} } class BasketBall{ private String brand; private double price; public BasketBall(){} public BasketBall(String brand, double price) {
super();
this.brand = brand;
this.price = price;
} @Override
public boolean equals(Object obj) { if(null!=obj&&obj.getClass()==getClass()){ BasketBall ball= (BasketBall) obj;
if(brand.equals(ball.brand)&&price==ball.price){ return true;
}
}
return false;
} }

上面算是比较正常的一个重写equals的方法,后续书本作者也提到了一个情况

就是对象的变量如果不是基础类型,也是引用类型的话,就需要额外处理了(举个例子,比如brand改成Stringbuffer类型)

作者给出了四个解决办法:

1.不使用Stringbuffer,继续使用String

2.比较的时候 先将Stringbuffer对象转出String(调用toString()方法)

3.继承Stringbuffer,重写equals方法 让变量变成重写类的类型(有点拗口)

4.放弃equals改用compare()方法

例子书本上都有,感兴趣的都可以去看看

关于何时重写equals,上一节已经表述了自己的观点,这里补充下原作者的观点:

实践11~15 略

3.异常处理

实践16:认识【异常控制流】机制

记住一个模式:try { //do something }catch(Exception e){// when exception happen  to do }finally{//不管有无异常 都将执行 不受return 影响}

顺序为:先执行try中的逻辑,如果正常执行,则跳转到finally块中执行,如果异常了,则会终止try块中的逻辑,转移到

catch块中执行,最后还是会在finally完成最后的操作

 /**
* @author lwx
* TODO
* 参考:
* 2014-5-13 下午4:40:36
*/
public class Lesson16 { public static void main(String[] args) { int i =0;
int k=2;
int addResult=0;
int divideResult=0;
try {
divideResult=k/i;
addResult=k+i;//这里将不会执行
} catch (Exception e) {
System.out.println("异常了-->"+e.getMessage());
}finally{ addResult=1;
divideResult=0;
}
System.out.println("addResult-->"+addResult);
System.out.println("divideResult-->"+divideResult); } }

实践17:绝对不可轻视异常

当发生程序异常的时候,我们有哪些处理方式

1.捕获并处理,防止它进一步传播

2.捕获并在此抛出它,传播给它的调用者

3.捕获它,并抛出一个新的异常给调用者

4.不捕获这个异常,任由它传播

这里需要用到引用一个新的概念:抛出异常,通过throws来完成

正常情况下,第一个处理方式是最常见的,实践16中也是采用了第一种处理方式

后续三种我们将在实践18~20中一一介绍

 public class Lesson17 {
public static void main(String[] args) {
try {
test();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println("异常处理提示");
}
} static void test () throws Exception{ System.out.println(2/0); throw new Exception(); } }

实践18:千万不要遮掩异常

在处理try块汇总的异常时,如果catch获取finally中又抛出异常,那么之前的异常会被覆盖

优先级:finally>catch>try

我们知道,finally是不受之前是否异常影响的,都将会执行,但是特殊情况下finally语句照样

也会产生异常,那到底要如何处理呢?那就是将异常存放在Vector中

 /**
*
* 如何捕获所有的异常 -->将异常对象存放在容器对象中
*
* @author lwx TODO 参考: 2014-5-13 下午5:02:51
*/
public class Lesson18 { public static void main(String[] args) { Hidden hidden = new Hidden();
try {
hidden.readFile();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//改进过的捕获异常的方式
NotHidden notHidden=new NotHidden(); try {
notHidden.readFile();
} catch (ReadFileException e) {
// TODO Auto-generated catch block
e.printStackTrace();
//捕获到的异常存放到容器中
System.out.println(e.exceptionVector().size()); }
} } class ReadFileException extends IOException {
private Vector excVector; public ReadFileException(Vector v) {
excVector = v; } public Vector exceptionVector() { return excVector;
} } class Hidden { void readFile() throws FileNotFoundException, IOException { BufferedReader br1 = null;
BufferedReader br2 = null;
FileReader fr = null; try {
fr = new FileReader("test.txt");
br1 = new BufferedReader(fr);
int i = br1.read(); fr = new FileReader("test2.txt");
br2 = new BufferedReader(fr);
i = br2.read(); } finally { if (null != br1) { br1.close();
}
if (null != br2) { br2.close();
} } } } class NotHidden { void readFile() throws ReadFileException { BufferedReader br1 = null;
BufferedReader br2 = null;
FileReader fr = null;
Vector excVec = new Vector(2); try {
fr = new FileReader("test.txt");
br1 = new BufferedReader(fr);
int i = br1.read(); fr = new FileReader("test2.txt");
br2 = new BufferedReader(fr);
i = br2.read();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
excVec.add(e);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
excVec.add(e);
} finally { if (null != br1) {
try {
br1.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
excVec.add(e);
}
}
if (null != br2) { try {
br2.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
excVec.add(e);
}
}
if(excVec.size()>0){
throw new ReadFileException(excVec);
}
} } }

实践19:明确throws字据的缺点

我们在开发过程中,经常会调用一些公用的函数(作者给了一个很通俗的名称:工蜂型函数),而这些函数

有可能会产生异常(比如调用数据库连接的方法),这时候处理异常有两种方式

一种是函数自身处理,一个是调用端来处理.个人偏好是函数本身来处理,否则有10处地方调用这个函数

就要捕获10次

实践20:细致而全面的理解throws子句

看的不是很懂,但是根据作者的demo,我理解的是作者想表达的意图是当重写函数的时候

函数抛出的异常范围不能大于该函数抛出范围,当然也可以不抛出异常(子类抛出异常的范围不能大于父类)

 /**
*
* 重写父类带有异常的方法
* 则子类中的方法抛出的异常必须是该异常或则该异常的父类(抛出异常范围更大)
* 当然 也可以不抛出异常
* @author lwx
* TODO
* 参考:
* 2014-5-13 下午7:02:57
*/
public class Lesson20 { public static void main(String[] args) throws IOException { ChildClass childClass =new ChildClass();
childClass.test();
childClass.test2();
childClass.test3();
}
} class SubClass{ public void test ()throws FileNotFoundException{ }
public void test2 ()throws FileNotFoundException{ }
public void test3 ()throws FileNotFoundException{ } }
class ChildClass{ public void test ()throws FileNotFoundException{ }
public void test2 ()throws IOException{ }
public void test3 (){ }
}

实践21:使用finally避免资源泄露

简单的描述就是在异常处理中,java规范是通过finally来做一些善后的事情(包括释放资源等)

实践23:将try/catch区段置于循环之外

弦外音:不能循环调用try/catch区段,而应该在一个try/catch中调用循环

实践27:抛出异常前线将对象恢复为有效状态

弦外音:将状态变量等处理放在可能处理异常之后,保证状态不受异常影响

4.性能

实践31:如欲进行字符串结合,StringBuffer优于String

弦外音:对于需要频繁处理字符拼接组合的地方,请使用StringBuffer,或则对于稍微复杂的操作,请使用StringBuffer

实践33:慎防未使用的对象

弦外音:如果一个判断两选一,请不要都创建之,然后根据if else 来选择其中一个对象

   换句话说,使用的时候才去创建对象

实践34:将同步化降至最低

弦外音:尽量少用同步操作,诸如synchronized等,除非你需要同步资源

实践35:尽可能使用stack变量

弦外音:尽量使用局部变量(stack)来代替全局变量(heap)

实践36:使用static,final和Privatae函数以促成inlining

略,后续在认真看

5.多线程

略,对多线程感兴趣的朋友可以看看,都比较基础

6.对象

实践59.运用接口来支持多继承

弦外音:java虽然不支持多继承(class a extends b,c)但是却可以通过接口

来完成多实现(class a implements a,b ,c......)

实践60.避免接口函数发生冲突

弦外音:假如类A实现了B,C两个接口,但是B和C都有方法test()

这样A要实现B还是C的test()方法呢,作者给出了解决方式,个人觉得方法很赞

java中很多库都使用了类似的方式来解决这类问题

即让D继承B接口(重命名接口,避免冲突),然后class A Implements D,C

实践61、62:

关于继承和接口 抽象类等知识点

实践63~66

关于类引用之间的操作 包括浅克隆 深克隆等

实践68:在构造函数内调用non-final函数是要当心

这个记得初学java时候,比较难搞懂的一道题目,了解其机制

需要明白java对一个类的初始过程

我们结合代码来看下

 /**
* 构建子类构造的时候会调用父类的构造函数
* 而父类的构造函数中有调用lookup() 注意 此时的loopup()调用的是子类的loopup()
* 而此时子类中的变量都还没初始化 都是默认值 因此num=0调用之后val也是0
* 下面打印的语句顺序可以参考 就明白了
* @author lwx
* @TODO 参考:
* @createtime 2014-5-13 下午7:44:35
*/
public class Lesson68 { public static void main(String[] args) { Derived derived=new Derived();
System.out.println(derived.value()); } } class Base { private int val; public Base() {
System.out.println(" Base()");
val = lookup();
} public int lookup() {
System.out.println(" Base lookup");
// TODO Auto-generated method stub
return 5;
} public int value() {
System.out.println(" Base value");
return val;
}
} class Derived extends Base{
private int num=10;
public int lookup(){
System.out.println(" Derived lookup");
System.out.println(" Derived lookup num=="+num);
return num; }
public Derived(){
System.out.println(" Derived()");
System.out.println(" Derived Derived() num=="+num);
} }

PDF和笔记源码下载地址:http://download.csdn.net/detail/draem0507/7342879

   

《practical Java》读书笔记的更多相关文章

  1. csapp读书笔记-并发编程

    这是基础,理解不能有偏差 如果线程/进程的逻辑控制流在时间上重叠,那么就是并发的.我们可以将并发看成是一种os内核用来运行多个应用程序的实例,但是并发不仅在内核,在应用程序中的角色也很重要. 在应用级 ...

  2. CSAPP 读书笔记 - 2.31练习题

    根据等式(2-14) 假如w = 4 数值范围在-8 ~ 7之间 2^w = 16 x = 5, y = 4的情况下面 x + y = 9 >=2 ^(w-1)  属于第一种情况 sum = x ...

  3. CSAPP读书笔记--第八章 异常控制流

    第八章 异常控制流 2017-11-14 概述 控制转移序列叫做控制流.目前为止,我们学过两种改变控制流的方式: 1)跳转和分支: 2)调用和返回. 但是上面的方法只能控制程序本身,发生以下系统状态的 ...

  4. CSAPP 并发编程读书笔记

    CSAPP 并发编程笔记 并发和并行 并发:Concurrency,只要时间上重叠就算并发,可以是单处理器交替处理 并行:Parallel,属于并发的一种特殊情况(真子集),多核/多 CPU 同时处理 ...

  5. 读书笔记汇总 - SQL必知必会(第4版)

    本系列记录并分享学习SQL的过程,主要内容为SQL的基础概念及练习过程. 书目信息 中文名:<SQL必知必会(第4版)> 英文名:<Sams Teach Yourself SQL i ...

  6. 读书笔记--SQL必知必会18--视图

    读书笔记--SQL必知必会18--视图 18.1 视图 视图是虚拟的表,只包含使用时动态检索数据的查询. 也就是说作为视图,它不包含任何列和数据,包含的是一个查询. 18.1.1 为什么使用视图 重用 ...

  7. 《C#本质论》读书笔记(18)多线程处理

    .NET Framework 4.0 看(本质论第3版) .NET Framework 4.5 看(本质论第4版) .NET 4.0为多线程引入了两组新API:TPL(Task Parallel Li ...

  8. C#温故知新:《C#图解教程》读书笔记系列

    一.此书到底何方神圣? 本书是广受赞誉C#图解教程的最新版本.作者在本书中创造了一种全新的可视化叙述方式,以图文并茂的形式.朴实简洁的文字,并辅之以大量表格和代码示例,全面.直观地阐述了C#语言的各种 ...

  9. C#刨根究底:《你必须知道的.NET》读书笔记系列

    一.此书到底何方神圣? <你必须知道的.NET>来自于微软MVP—王涛(网名:AnyTao,博客园大牛之一,其博客地址为:http://anytao.cnblogs.com/)的最新技术心 ...

  10. Web高级征程:《大型网站技术架构》读书笔记系列

    一.此书到底何方神圣? <大型网站技术架构:核心原理与案例分析>通过梳理大型网站技术发展历程,剖析大型网站技术架构模式,深入讲述大型互联网架构设计的核心原理,并通过一组典型网站技术架构设计 ...

随机推荐

  1. Cesium随笔(5)CZML介绍(介个文章是转的嘿嘿)【转】

    通过czml可以在cesium上实现非常棒的动态效果 (1)Cesium Language (CZML) 入门--CZML Structure(CZML的结构) 原文地址:https://github ...

  2. 添加sqljdbc4的maven依赖

    sqljdbc是微软sql server的jdbc驱动 使用sqljdbc需要从微软的官方网站下载jar包: http://www.microsoft.com/en-us/download/detai ...

  3. Realm Swift

    Realm Swift 当前这个翻译,主要是方便我自己查阅api,有非常多地方写的比較晦涩或者没有翻译,敬请谅解 version 0.98.7 官方文档 參考文献 Realm支持类型 String,N ...

  4. Jmeter测试报告可视化(Excel, html以及jenkins集成)

    做性能测试通常在none GUI的命令行模式下运行Jmeter. 例如: jmeter -n -t /opt/las/JMeter/TestPlan/test.jmx -l /opt/las/JMet ...

  5. DigCSDN介绍首页

    recrefer=SE_D_DigCSDN">360手机助手下载地址 兴许平台会陆续登陆上线的,大家敬请期待 最后由于屏幕适配和分享链接的问题,导致最后的公布时间延误了好几天--- 今 ...

  6. qtp:vbs基础教程

    ◎Vbs脚本编程简明教程之中的一个-为什么要使用Vbs?  在Windows中,学习计算机操作或许非常easy,可是非常多计算机工作是反复性劳动,比如你每周或许须要对一些计算机文件进行复制.粘贴.改名 ...

  7. android 时间与String的相互转化

    :大体思路 [html] view plaincopy 这种转换要用到java.text.SimpleDateFormat类 字符串转换成日期类型: 方法1: 也是最简单的方法 Date date=n ...

  8. 把普通java项目转换成maven项目

    我使用的是eclipse,右键项目,Configure->Convert to Maven Project 然后就是jar包的引入了,如果jar包比较简单,建议从maven中拉取,这样便于后期升 ...

  9. JSTL详解(一)

    将jstl.jar包导入到工程中 jstldemo1.jsp <%@ taglib prefix="c" uri="http://java.sun.com/jsp/ ...

  10. Linux常用shell脚本

    在运维中,尤其是linux运维,都知道脚本的重要性,脚本会让我们的 运维事半功倍,所以学会写脚本是我们每个linux运维必须学会的一门功课,如何学好脚本,最关键的是就是大量的练习 和实践. 1.用Sh ...