lxf-java selected study
1.Java快速入门之数组操作
1.1遍历数组
1.像c语言那般的for循环
2.for each循环
直接迭代数组中的每个元素
public static void main(String[] args) {int[] nb={2,4,6,7,8};for (int i:nb) {System.out.println(i);}
//将数组中的每个元素 依次拿出来给到 变量i , 再执行循环体
1.2数组排序
1.可自己写排序算法
2.在idea中,可使用Arrays.sort(数组)直接排序(小到大)
注意:打印数组时,可直接用:System.out.println(Arrays.toString(数组));
2.面向对象编程
面向对象编程,英文是Object-Oriented Programming,简称OOP。
什么是面向对象:
和面向对象编程不同的,是面向过程编程。面向过程编程,是把模型分解成一步一步的过程。比如,老板告诉你,要编写一个TODO任务,必须按照以下步骤一步一步来:
- 读取文件;
- 编写TODO;
- 保存文件。
面向对象编程,是一种通过对象的方式,把现实世界映射到计算机模型的一种编程方法。
2.1面向对象基础
类class和实例instance
class是一种对象模版,它定义了如何创建实例,因此,class本身就是一种数据类型。
而instance是对象实例,instance是根据class创建的实例,可以创建多个instance,每个instance类型相同,但各自属性可能不相同
Person ming = new Person();
注意区分:Person ming
是定义Person
类型的变量ming
,而new Person()
是创建Person
实例。
一个Java源文件可以包含多个类的定义,但只能定义一个public类,且public类名必须与文件名一致。如果要定义多个public类,必须拆到多个Java源文件中。
1.方法
一个class中可以有多个field(属性)。例如,给Person类定义两个field(属性)
Class Person{public String name;public int age;
}
但要注意,把类中的filed(属性)的访问权限设为public,属性会暴露给外部,很可能会破坏封装性。即外部可以直接对Person类中的field(属性)进行修改:
Person ming = new Person();
ming.name = "Xiao Ming";
ming.age = -99; // age设置为负数
显然,直接操作field(属性),容易造成逻辑混乱。
为避免外部代码直接访问类中的属性field,可用private修饰field,拒绝外部访问:
class Person{private String name;private int age;
}
外部代码无法操作类 中的属性,那么创建对象后,如何给其属性赋值??
可以在类中定义方法,方法的传参作为所想要赋的值,
class Person{private String name;private int age;public void setName(String name) {this.name = name;}public void setAge(int age) {this.age = age;}public String getName() {return name;}public int getAge() {return age;}
显然,age传入的参数可能是负数,负数应该是无效值,故应对传入的参数进行判断
public void setAge(int age){if(age<0){System.out.println("age输入值无效”);return;}this.age=age;
外部代码不能直接修改private
字段,但是,外部代码可以调用方法setName()
和setAge()
来间接修改private
字段。
同样,外部代码不能直接读取private
字段,但可以通过getName()
和getAge()
间接获取private
字段的值。
所以,一个类通过定义方法,就可以给外部代码暴露一些操作的接口,同时,内部自己保证逻辑一致性。
定义方法
定义方法的语法是:
修饰符 方法返回类型 方法名(方法参数列表) {若干方法语句;return 方法返回值;
}
this变量
在方法内部,可以使用一个隐含的变量this
,它始终指向当前实例。因此,通过this.field
就可以访问当前实例的字段。
如果没有命名冲突,可以省略this
。例如:
class Person {private String name;public String getName() {return name; // 相当于this.name}
}
但是,如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this
:
class Person {private String name;public void setName(String name) {this.name = name; // 前面的this不可少,少了就变成局部变量name了}
}
构造方法
创建实例的时候,实际上是通过构造方法来初始化实例的。
由于构造方法是如此特殊,所以构造方法的名称就是类名。构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有void
),调用构造方法,必须用new
操作符。
也就是说:new对象时,会自动调用构造方法。
在Java中,创建对象实例的时候,按照如下顺序进行初始化:
先初始化字段,例如,
int age = 10;
表示字段初始化为10
,double salary;
表示字段默认初始化为0
,String name;
表示引用类型字段默认初始化为null
;执行构造方法的代码进行初始化。
方法重载(overload)
在一个类中,我们可以定义多个方法。如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。
这种方法名相同,但各自的参数不同,称为方法重载(Overload
)。
方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此,调用起来更简单。
继承
继承是面向对象编程中非常强大的一种机制,它首先可以复用代码。当我们让Student
从Person
继承时,Student
就获得了Person
的所有功能,我们只需要为Student
编写新增的功能。
Java使用extends
关键字来实现继承:
public class Person {private String name;private int age;public void setName(String name) {this.name = name;}public void setAge(int age) {this.age = age;}public String getName() {return name;}public int getAge() {return age;}
}
public class Student extends Person{private int score;public int getScore() {return score;}public void setScore(int score) {this.score = score;}
}
注意:子类自动获得了父类的所有字段,严禁定义与父类重名的字段!
在OOP的术语中,我们把Person
称为超类(super class),父类(parent class),基类(base class),把Student
称为子类(subclass),扩展类(extended class)。
继承树
注意到我们在定义Person
的时候,没有写extends
。在Java中,没有明确写extends
的类,编译器会自动加上extends Object
。所以,任何类,除了Object
,都会继承自某个类。下图是Person
、Student
的继承树:
┌───────────┐
│ Object │
└───────────┘▲│
┌───────────┐
│ Person │
└───────────┘▲│
┌───────────┐
│ Student │
Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object
特殊,它没有父类。 Java中没有 多继承!!!
类似的,如果我们定义一个继承自Person
的Teacher
,它们的继承树关系如下:
┌───────────┐│ Object │└───────────┘▲│┌───────────┐│ Person │└───────────┘▲ ▲│ ││ │
┌───────────┐ ┌───────────┐
│ Student │ │ Teacher │
└───────────┘ └───────────┘
protected
继承有个特点,就是子类无法访问父类的private
字段或者private
方法。例如,Student
类就无法访问Person
类的name
和age
字段:
class Person {private String name;private int age;
}class Student extends Person {public String hello() {return "Hello, " + name; // 编译错误:无法访问name字段}
}
这使得继承的作用被削弱了。为了让子类可以访问父类的字段,我们需要把private
改为protected
。用protected
修饰的字段可以被子类访问:
一个protected
字段和方法可以被其子类,以及子类的子类所访问。
向上转型(父管子√)
如果一个引用变量的类型是Student
,那么它可以指向一个Student
类型的实例:
Student s = new Student();s是引用型变量,new是实例化对象
如果一个引用类型的变量是Person
,那么它可以指向一个Person
类型的实例:
Person p = new Person();
如果Student
是从Person
继承下来的,那么,一个引用类型为Person
的变量,能否指向Student
类型的实例?Person p = new Student()???;--------答案是可以
这是因为Student
继承自Person
,因此,它拥有Person
的全部功能。Person
类型的变量,如果指向Student
类型的实例,对它进行操作,是没有问题的!
这种把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。
父管子------向上转型
向上转型实际上是把一个子类型安全地变为更加抽象的父类型:
为什么说是 更抽象的父类 呢? 细想:子类从父类那继承,得到父类的所有东西,且能自己另外添加别的内容,那么, 子类 就比 父类 多内容----更具体,,相对地,父类相比子类,更抽象。
向下转型
和向上转型相反,如果把一个父类类型强制转型为子类类型,就是向下转型(downcasting)。
需注意的是,若一个父类变量 本身管一个 子类对象(向上转型),父管子,ok,这时,若它该父类变量 强转为 子类类型变量, 是可以的。
若一个父类变量 本身管一个 父类对象,,若把该父类变量 强制为 子类类型对象 交由 子类变量管理----这是不行的!
向下转型----子管父(要想成功,父原先是管子的才行)
多态
在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。
方法签名:方法名称和参数列表(方法的参数的顺序和类型)组成
Override和Overload不同的是,如果方法签名不同(特指参数列表不同),就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override
。
override是全部相同 才 叫override(覆盖--全部相同才覆盖)
overload是参数列表不同 (重载--定是因为有不同才需要重新加载)
public class Main {public static void main(String[] args) {Person p = new Student();p.run(); // 应该打印Person.run还是Student.run?}
}class Person {public void run() {System.out.println("Person.run");}
}class Student extends Person {@Overridepublic void run() {System.out.println("Student.run");}
}
-------这里我们能看到变量p管理的实际上是个Student的对象,故打印Student.run------
运行一下上面的代码就可以知道,实际上调用的方法是Student
的run()
方法。因此可得出结论:
Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。
这个非常重要的特性在面向对象编程中称之为多态。
多态是指,针对某个类型的方法调用,其 真正执行的方法 取决于 运行时期 实际类型的方法。
例如:Person p = new Student(); p.run;//无法确定运行时究竟调用哪个run()方法
有童鞋会问,从上面的代码一看就明白,肯定调用的是Student
的run()
方法啊。
但是,假设我们编写这样一个方法:
public void runTwice(Person p) {p.run();p.run();
}
它传入的参数类型是Person
,我们是无法知道传入的参数实际类型究竟是Person
,还是Student
,还是Person
的其他子类(因为Java中,可向上造型,让父类变量管理子类的对象),因此,也无法确定调用的是不是Person
类定义的run()
方法。(Person p---一个父类变量,我门无法得知他实际管理的对象 是 Person类型 还是 Student类型 抑或者是Teacher类型)
所以,多态的特性就是,运行时才能动态决定调用的子类方法。
对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法。
这种不确定性的方法调用,究竟有什么作用?
我们还是来举栗子。
假设我们定义一种收入,需要给它报税,那么先定义一个Income
类:
class Income{protected double income;public double getTax(){return income*0.1; //税率百分之10}}
对于工资收入,可以减去一个基数,那么我们可以从Income
派生出SalaryIncome
,并覆写getTax()
:
public class SlartIncome extends Income{@Overridepublic double getTax(){if(income<=5000){return 0;}else{return (income-5000)*0.2;}}
}
如果你享受国务院特殊津贴,那么按照规定,可以全部免税:
public class StateCouncilSpecialAllowance extends Income{@Overridepublic double getTax(){return 0;}
}
现在,我们要编写一个报税的财务软件,对于一个人的所有收入进行报税,可以这么写:
public double totalTax(Income... incomes) {double total = 0;for (Income income: incomes) {total = total + income.getTax();}return total;
}
public class Main {public static void main(String[] args) {// 给一个有普通收入、工资收入和享受国务院特殊津贴的小伙伴算税:Income[] incomes = new Income[] {new Income(3000),new Salary(7500),new StateCouncilSpecialAllowance(15000)};System.out.println(totalTax(incomes));}public static double totalTax(Income... incomes) {double total = 0;for (Income income: incomes) {total = total + income.getTax();}return total;}
}class Income {protected double income;public Income(double income) {this.income = income;}public double getTax() {return income * 0.1; // 税率10%}
}class Salary extends Income {public Salary(double income) {super(income);}@Overridepublic double getTax() {if (income <= 5000) {return 0;}return (income - 5000) * 0.2;}
}class StateCouncilSpecialAllowance extends Income {public StateCouncilSpecialAllowance(double income) {super(income);}@Overridepublic double getTax() {return 0;}
}
观察totalTax()
方法:利用多态,totalTax()
方法只需要和Income
打交道,它完全不需要知道Salary
和StateCouncilSpecialAllowance
的存在,就可以正确计算出总的税。
如果我们要新增一种稿费收入,只需要从Income
派生,然后正确覆写getTax()
方法就可以。
把新的类型传入totalTax()
,不需要修改任何代码。
调用super
在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super
来调用。
final
继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final
。用final
修饰的方法不能被Override
如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final
。用final
修饰的类不能被继承.
抽象类
由于多态的存在,每个子类都可以覆写父类的方法,例如:
class Person {public void run() { … }
}class Student extends Person {@Overridepublic void run() { … }
}class Teacher extends Person {@Overridepublic void run() { … }
}
从Person
类派生的Student
和Teacher
都可以覆写run()
方法。
如果父类Person
的run()
方法没有实际意义,能否去掉方法的执行语句?
class Person {public void run(); // Compile Error!
}
答案是不行,会导致编译错误,因为定义方法的时候,必须实现方法的语句。
能不能去掉父类的run()
方法?
答案还是不行,因为去掉父类的run()
方法,就失去了多态的特性。例如,runTwice()
就无法编译:
public void runTwice(Person p) {p.run(); // Person没有run()方法,会导致编译错误p.run();
}
如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法:
class Person {public abstract void run();
}
把一个方法声明为abstract
,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person
类也无法被实例化。编译器会告诉我们,无法编译Person
类,因为它包含抽象方法。
必须把Person
类本身也声明为abstract
,才能正确编译它:
abstract class Person {public abstract void run();
}
一个类中含有一个抽象方法,,那么,这个类 也必须 定义为 抽象类 才能编译成功!
无法实例化的抽象类有什么用?
因为抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。因此,抽象方法实际上相当于定义了“规范”。
例如,Person
类定义了抽象方法run()
,那么,在实现子类Student
的时候,就必须覆写run()
方法:
面向抽象编程
当我们定义了抽象类Person
,以及具体的Student
、Teacher
子类的时候,我们可以通过抽象类Person
类型去引用具体的子类的实例:
Person s = new Student();
Person t = new Teacher();
这种引用抽象类的好处在于,我们对其进行方法调用,并不关心Person
类型变量的具体子类型:
// 不关心Person变量的具体子类型:
s.run();
t.run();
同样的代码,如果引用的是一个新的子类,我们仍然不关心具体类型:
// 同样不关心新的子类是如何实现run()方法的:
Person e = new Employee();
e.run();
这种尽量 引用 高层类型,避免 引用 实际子类型 的方式,称之为面向抽象编程。
面向抽象编程的本质就是:
上层代码只定义规范(例如:
abstract class Person
);不需要子类就可以实现业务逻辑(正常编译);
具体的业务逻辑由不同的子类实现,调用者并不关心。
小结
通过
abstract
定义的方法是抽象方法,它只有定义,没有实现。抽象方法定义了子类必须实现的接口规范;定义了抽象方法的class必须被定义为抽象类,从抽象类继承的子类必须实现抽象方法;
如果不实现抽象方法,则该子类仍是一个抽象类;
面向抽象编程使得调用者只关心抽象方法的定义,不关心子类的具体实现。
接口
在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。
如果一个抽象类 没有 字段(属性),所有方法全部都是抽象方法:
(上面变相说明抽象类可以有字段)
abstract class Person {public abstract void run();public abstract String getName();
}
就可以把该抽象类改写为接口:interface
。
在Java中,使用interface
可以声明一个接口:
interface Person {void run();String getName();
}
对比上面两部分的代码,接口这里 连抽象类方法的 public abstract 都能省略了
所谓interface
,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。
因为接口定义的所有方法默认都是public abstract
的,所以这两个修饰符不需要写出来(写不写效果都一样)。
当一个具体的class
去实现一个interface
时,需要使用implements
关键字。
在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface。
继承关系
合理设计interface
和abstract class
的继承关系,可以充分复用代码。
一般来说,公共逻辑适合放在abstract class
中,具体逻辑放到各个子类,而接口层次代表抽象程度。可以参考Java的集合类定义的一组接口、抽象类以及具体子类的继承关系:
┌───────────────┐
│ Iterable │
└───────────────┘▲ ┌───────────────────┐│ │ Object │
┌───────────────┐ └───────────────────┘
│ Collection │ ▲
└───────────────┘ │▲ ▲ ┌───────────────────┐│ └──────────│AbstractCollection │
┌───────────────┐ └───────────────────┘
│ List │ ▲
└───────────────┘ │▲ ┌───────────────────┐└──────────│ AbstractList │└───────────────────┘▲ ▲│ ││ │┌────────────┐ ┌────────────┐│ ArrayList │ │ LinkedList │└────────────┘ └────────────┘
在使用的时候,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象:
List list = new ArrayList(); // 用List接口引用具体子类的实例
Collection coll = list; // 向上转型为Collection接口
Iterable it = coll; // 向上转型为Iterable接口
default方法
在接口中,可以定义default
方法。例如,把Person
接口的run()
方法改为default
方法:
public class Main {public static void main(String[] args) {Person p = new Student("Xiao Ming");p.run();}
}interface Person {String getName();default void run() {System.out.println(getName() + " run");}
}class Student implements Person {private String name;public Student(String name) {this.name = name;}public String getName() {return this.name;}
}
实现类可以不必覆写default
方法。default
方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default
方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
default
方法和抽象类的普通方法是有所不同的。因为interface
没有字段,default
方法无法访问字段,而抽象类的普通方法可以访问实例字段。
小结
Java的接口(interface)定义了纯抽象规范,一个类可以实现多个接口;
接口也是数据类型,适用于向上转型和向下转型;
接口的所有方法都是抽象方法,接口不能定义实例字段;
接口可以定义default
方法(JDK>=1.8)。
静态字段和静态方法
在一个class
中定义的字段,我们称之为实例字段。实例字段的特点是,每个实例都有独立的字段,各个实例的同名字段互不影响。
还有一种字段,是用static
修饰的字段,称为静态字段:static field
。
实例字段在每个实例中都有自己的一个独立“空间”,但是静态字段只有一个共享“空间”,所有实例都会共享该字段。举个例子:
public class Main {public static void main(String[] args) {Person ming = new Person("Xiao Ming", 12);Person hong = new Person("Xiao Hong", 15);ming.number = 88;System.out.println(hong.number);hong.number = 99;System.out.println(ming.number);}
}class Person {public String name;public int age;public static int number;public Person(String name, int age) {this.name = name;this.age = age;}
}
对于静态字段,无论修改哪个实例的静态字段,效果都是一样的:所有实例的静态字段都被修改了,原因是静态字段并不属于实例:
┌──────────────────┐
ming ──▶│Person instance │├──────────────────┤│name = "Xiao Ming"││age = 12 ││number ───────────┼──┐ ┌─────────────┐└──────────────────┘ │ │Person class ││ ├─────────────┤├───▶│number = 99 │┌──────────────────┐ │ └─────────────┘
hong ──▶│Person instance │ │├──────────────────┤ ││name = "Xiao Hong"│ ││age = 15 │ ││number ───────────┼──┘└──────────────────┘
虽然实例可以访问静态字段,但是它们指向的其实都是Person class
的静态字段。所以,所有实例共享一个静态字段。
因此,不推荐用实例变量.静态字段
去访问静态字段,因为在Java程序中,实例对象并没有静态字段。在代码中,实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为类名.静态字段
来访问静态对象。
推荐用类名来访问静态字段。可以把静态字段理解为描述class
本身的字段(非实例字段)。对于上面的代码,更好的写法是:
Person.number = 99;
System.out.println(Person.number);
静态方法
有静态字段,就有静态方法。用static
修饰的方法称为静态方法。
调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用。静态方法类似其它编程语言的函数。例如:
public class Main {public static void main(String[] args) {Person.setNumber(99);System.out.println(Person.number);}
}class Person {public static int number;public static void setNumber(int value) {number = value;}
}
因为静态方法属于class
而不属于实例,因此,静态方法内部,无法访问this
变量,也无法访问实例字段,它只能访问静态字段。
通过 实例变量 也 可以调用 静态方法,但 这只是 编译器 自动 帮我们 把实例改写成类名 而已。
通常情况下,通过实例变量访问静态字段和静态方法,会得到一个编译警告。
接口的静态字段
因为interface
是一个纯抽象类,所以它不能定义实例字段。但是,interface
是可以有静态字段的,并且静态字段必须为final
类型:
public interface Person {public static final int MALE = 1;public static final int FEMALE = 2;
}
实际上,因为interface
的字段只能是public static final
类型,所以我们可以把这些修饰符都去掉,上述代码可以简写为:
public interface Person {// 编译器会自动加上public statc final:int MALE = 1;int FEMALE = 2;
}
编译器会自动把该字段变为public static final
类型。
小结
静态字段属于所有实例“共享”的字段,实际上是属于
class
的字段;调用静态方法不需要实例,无法访问
this
,但可以访问静态字段和其他静态方法;静态方法常用于工具类和辅助方法。
包
在前面的代码中,我们把类和接口命名为Person
、Student
、Hello
等简单名字。
在现实中,如果小明写了一个Person
类,小红也写了一个Person
类,现在,小白既想用小明的Person
,也想用小红的Person
,怎么办?
如果小军写了一个Arrays
类,恰好JDK也自带了一个Arrays
类,如何解决类名冲突?
在Java中,我们使用package
来解决名字冲突。
Java定义了一种名字空间,称之为包:package
。一个类总是属于某个包,类名(比如Person
)只是一个简写,真正的完整类名是包名.类名
。
例如:
小明的Person
类存放在包ming
下面,因此,完整类名是ming.Person
;
小红的Person
类存放在包hong
下面,因此,完整类名是hong.Person
;
小军的Arrays
类存放在包mr.jun
下面,因此,完整类名是mr.jun.Arrays
;
JDK的Arrays
类存放在包java.util
下面,因此,完整类名是java.util.Arrays
。
在定义class
的时候,我们需要在第一行声明这个class
属于哪个包。
小明的Person.java
文件:
package ming; // 申明包名mingpublic class Person {
}
在Java虚拟机执行的时候,JVM只看完整类名,因此,只要包名不同,类就不同。
包可以是多层结构,用.
隔开。例如:java.util
。
要特别注意:包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。
我们还需要按照包结构把上面的Java文件组织起来。假设以package_sample
作为根目录,src
作为源码目录,那么所有文件结构就是:
package_sample
└─ src├─ hong│ └─ Person.java│ ming│ └─ Person.java└─ mr└─ jun└─ Arrays.java
即所有Java文件对应的目录层次要和包的层次一致。
编译后的.class
文件也需要按照包结构存放。如果使用IDE,把编译后的.class
文件放到bin
目录下,那么,编译的文件结构就是:
package_sample
└─ bin├─ hong│ └─ Person.class│ ming│ └─ Person.class└─ mr└─ jun└─ Arrays.class
包作用域
位于同一个包的类,可以访问包作用域的字段和方法。不用public
、protected
、private
修饰的字段和方法就是包作用域。
------------------简而言之:就是同一个包下的不同类,可以互相使用。
import
在一个class
中,我们总会引用其他的class
。例如,小明的ming.Person
类,如果要引用小军的mr.jun.Arrays
类,他有三种写法:
第一种,直接写出完整类名,例如:
// Person.java
package ming;public class Person {public void run() {mr.jun.Arrays arrays = new mr.jun.Arrays();}
}
很显然,每次写完整类名比较痛苦。
因此,第二种写法是用import
语句,导入小军的Arrays
,然后写简单类名:
// Person.java
package ming;// 导入完整类名:
import mr.jun.Arrays;public class Person {public void run() {Arrays arrays = new Arrays();}
}
Java编译器最终编译出的.class
文件只使用完整类名,因此,在代码中,当编译器遇到一个class
名称时:
如果是完整类名,就直接根据完整类名查找这个
class
;如果是简单类名,按下面的顺序依次查找:
查找当前
package
是否存在这个class
;查找
import
的包是否包含这个class
;查找
java.lang
包是否包含这个class
。
如果按照上面的规则还无法确定类名,则编译报错。
因此,编写class的时候,编译器会自动帮我们做两个import动作:
默认自动
import
当前package
的其他class
;默认自动
import java.lang.*
。
注意:自动导入的是java.lang包,但类似java.lang.reflect这些包仍需要手动导入。
作用域
在Java中,我们经常看到public
、protected
、private
这些修饰符。在Java中,这些修饰符可以用来限定访问作用域。
public
定义为public
的class
、interface
可以被其他任何类访问;
定义为public
的field
、method
可以被其他类访问,前提是首先有访问class
的权限;
private
定义为private
的field
、method
无法被其他类访问;
实际上,确切地说,private
访问权限被限定在class
的内部,而且与方法声明顺序无关。推荐把private
方法放到后面,因为public
方法定义了类对外提供的功能,阅读代码的时候,应该先关注public
方法;
protected
protected
作用于继承关系。定义为protected
的字段和方法可以被子类访问,以及子类的子类;
package
最后,包作用域是指一个类允许访问同一个package
的没有public
、private
修饰的class
,以及没有public
、protected
、private
修饰的字段和方法。
注意,包名必须完全一致,包没有父子关系,com.apache
和com.apache.abc
是不同的包。
局部变量
在方法内部定义的变量称为局部变量,局部变量作用域从变量声明处开始到对应的块结束。方法参数也是局部变量。
package abc;public class Hello {void hi(String name) { // ①String s = name.toLowerCase(); // ②int len = s.length(); // ③if (len < 10) { // ④int p = 10 - len; // ⑤for (int i=0; i<10; i++) { // ⑥System.out.println(); // ⑦} // ⑧} // ⑨} // ⑩
}
我们观察上面的hi()
方法代码:
方法参数name是局部变量,它的作用域是整个方法,即①~⑩;
变量s的作用域是定义处到方法结束,即②~⑩;
变量len的作用域是定义处到方法结束,即③~⑩;
变量p的作用域是定义处到if块结束,即⑤~⑨;
变量i的作用域是for循环,即⑥~⑧。
使用局部变量时,应该尽可能把局部变量的作用域缩小,尽可能延后声明局部变量。
final
Java还提供了一个final
修饰符。final
与访问权限不冲突,它有很多作用。
用final
修饰class
可以阻止被继承;
用final
修饰method
可以阻止被子类覆写;
用final
修饰field
可以阻止被重新赋值;
用final
修饰局部变量可以阻止被重新赋值;
最佳实践
如果不确定是否需要public
,就不声明为public
,即尽可能少地暴露对外的字段和方法。
把方法定义为package
权限有助于测试,因为测试类和被测试类只要位于同一个package
,测试代码就可以访问被测试类的package
权限方法。
一个.java
文件只能包含一个public
类,但可以包含多个非public
类。如果有public
类,文件名必须和public
类的名字相同。
小结
Java内建的访问权限包括public
、protected
、private
和package
权限;
Java在方法内部定义的变量是局部变量,局部变量的作用域从变量声明开始,到一个块结束;
final
修饰符不是访问权限,它可以修饰class
、field
和method
;
一个.java
文件只能包含一个public
类,但可以包含多个非public
类。
jar包
如果有很多.class
文件,散落在各层目录中,肯定不便于管理。如果能把目录打一个包,变成一个文件,就方便多了。
jar包就是用来干这个事的,它可以把package
组织的目录层级,以及各个目录下的所有文件(包括.class
文件和其他文件)都打成一个jar文件,这样一来,无论是备份,还是发给客户,就简单多了。
jar包实际上就是一个zip格式的压缩文件,而jar包相当于目录。如果我们要执行一个jar包的class
,就可以把jar包放到classpath
中:
java -cp ./hello.jar abc.xyz.Hello
这样JVM会自动在hello.jar
文件里去搜索某个类。
模块
从Java 9开始,JDK又引入了模块(Module)。
什么是模块?这要从Java 9之前的版本说起。
我们知道,.class
文件是JVM看到的最小可执行文件,而一个大型程序需要编写很多Class,并生成一堆.class
文件,很不便于管理,所以,jar
文件就是class
文件的容器。
在Java 9之前,一个大型Java程序会生成自己的jar文件,同时引用依赖的第三方jar文件,而JVM自带的Java标准库,实际上也是以jar文件形式存放的,这个文件叫rt.jar
,一共有60多M。
如果是自己开发的程序,除了一个自己的app.jar
以外,还需要一堆第三方的jar包,运行一个Java程序,一般来说,命令行写这样:
java -cp app.jar:a.jar:b.jar:c.jar com.liaoxuefeng.sample.Main
注意:JVM自带的标准库rt.jar不要写到classpath中,写了反而会干扰JVM的正常运行。
如果漏写了某个运行时需要用到的jar,那么在运行期极有可能抛出ClassNotFoundException
。
所以,jar只是用于存放class的容器,它并不关心class之间的依赖。
从Java 9开始引入的模块,主要是为了解决“依赖”这个问题。如果a.jar
必须依赖另一个b.jar
才能运行,那我们应该给a.jar
加点说明啥的,让程序在编译和运行的时候能自动定位到b.jar
,这种自带“依赖关系”的class容器就是模块。
把一堆class封装为jar仅仅是一个打包的过程,而把一堆class封装为模块则不但需要打包,还需要写入依赖关系,并且还可以包含二进制代码(通常是JNI扩展)。
编写模块
那么,我们应该如何编写模块呢?还是以具体的例子来说。首先,创建模块和原有的创建Java项目是完全一样的,以oop-module
工程为例,它的目录结构如下:
oop-module
├── bin
├── build.sh
└── src├── com│ └── itranswarp│ └── sample│ ├── Greeting.java│ └── Main.java└── module-info.java
其中,bin
目录存放编译后的class文件,src
目录存放源码,按包名的目录结构存放,仅仅在src
目录下多了一个module-info.java
这个文件,这就是模块的描述文件。在这个模块中,它长这样:
module hello.world {requires java.base; // 可不写,任何模块都会自动引入java.baserequires java.xml;
}
其中,module
是关键字,后面的hello.world
是模块的名称,它的命名规范与包一致。花括号的requires xxx;
表示这个模块需要引用的其他模块名。除了java.base
可以被自动引入外,这里我们引入了一个java.xml
的模块。
(hello这个模块需要依赖java.xml ,在hello模块中声明它需要 依赖于 java.xml)
当我们使用模块声明了依赖关系后,才能使用引入的模块。例如,Main.java
代码如下:
package com.itranswarp.sample;// 必须引入java.xml模块后才能使用其中的类:
import javax.xml.XMLConstants;public class Main {public static void main(String[] args) {Greeting g = new Greeting();System.out.println(g.hello(XMLConstants.XML_NS_PREFIX));}
}
如果把requires java.xml;
从module-info.java
中去掉,编译将报错。可见,模块的重要作用就是声明依赖关系。
Java的类型
Java核心类
字符串和编码
在Java中,String
是一个引用类型,它本身也是一个class
。但是,Java编译器对String
有特殊处理,即可以直接用"..."
来表示一个字符串:
String s1 = "Hello!";
实际上字符串在String
内部是通过一个char[]
数组表示的,因此,按下面的写法也是可以的:
String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'});
Java字符串的一个重要特点就是字符串不可变。
包装类型
我们已经知道,Java的数据类型分两种:
基本类型:
byte
,short
,int
,long
,boolean
,float
,double
,char
引用类型:所有
class
和interface
类型
引用类型可以赋值为null
,表示空,但基本类型不能赋值为null;
么,如何把一个基本类型视为对象(引用类型)?
比如,想要把int
基本类型变成一个引用类型,我们可以定义一个Integer
类,它只包含一个实例字段int
,这样,Integer
类就可以视为int
的包装类(Wrapper Class):
public class Integer {private int value;public Integer(int value) {this.value = value;}public int intValue() {return this.value;}
}
定义好了Integer
类,我们就可以把int
和Integer
互相转换:
Integer n = null;
Integer n2 = new Integer(99);
int n3 = n2.intValue();
实际上,因为包装类型非常有用,Java核心库为每种基本类型都提供了对应的包装类型:
不变类
所有的包装类型都是不变类。我们查看Integer
的源码可知,它的核心代码如下:
public final class Integer {private final int value;
}
因此,一旦创建了Integer
对象,该对象就是不变的。
对两个Integer
实例进行比较要特别注意:绝对不能用==
比较,因为Integer
是引用类型,必须使用equals()
比较;
JavaBean
在Java中,有很多class
的定义都符合这样的规范:
- 若干
private
实例字段; - 通过
public
方法来读写实例字段。
例如:
public class Person {private String name;private int age;public String getName() { return this.name; }public void setName(String name) { this.name = name; }public int getAge() { return this.age; }public void setAge(int age) { this.age = age; }
}
如果读写方法符合以下这种命名规范:
// 读方法:
public Type getXyz()
// 写方法:
public void setXyz(Type value)
那么这种class
被称为JavaBean。
上面的字段是xyz
,那么读写方法名分别以get
和set
开头,并且后接大写字母开头的字段名Xyz
,因此两个读写方法名分别是getXyz()
和setXyz()
。
我们通常把一组对应的读方法(getter
)和写方法(setter
)称为属性(property
)。例如,name
属性:
- 对应的读方法是
String getName()
- 对应的写方法是
setName(String)
只有getter
的属性称为只读属性(read-only),例如,定义一个age只读属性:
- 对应的读方法是
int getAge()
- 无对应的写方法
setAge(int)
类似的,只有setter
的属性称为只写属性(write-only)。
很明显,只读属性很常见,只写属性不常见。
属性只需要定义getter
和setter
方法,不一定需要对应的字段。例如,child
只读属性定义如下:
public class Person {private String name;private int age;public String getName() { return this.name; }public void setName(String name) { this.name = name; }public int getAge() { return this.age; }public void setAge(int age) { this.age = age; }public boolean isChild() {return age <= 6;}
}
可以看出,getter
和setter
也是一种数据封装的方法。
JavaBean的作用
JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。此外,JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中。
通过IDE,可以快速生成getter
和setter
。
小结
JavaBean是一种符合命名规范的class
,它通过getter
和setter
来定义属性;
属性是一种通用的叫法,并非Java语法规定;
异常处理
在计算机程序运行的过程中,总是会出现各种各样的错误。
有一些错误是用户造成的;
还有一些错误是随机出现,并且永远不可能避免的。比如:
- 网络突然断了,连接不到远程服务器;
- 内存耗尽,程序崩溃了;
- 用户点“打印”,但根本没有打印机;
所以,一个健壮的程序必须处理各种各样的错误。
所谓错误,就是程序调用某个函数的时候,如果失败了,就表示出错。
调用方如何获知调用失败的信息?有两种方法:
方法一:约定返回错误码
方法二:在语言层面上提供一个异常处理机制。
Java内置了一套异常处理机制,总是使用异常来表示错误。
异常是一种class
,因此它本身带有类型信息。异常可以在任何地方抛出,但只需要在上层捕获,这样就和方法调用分离了:
try {String s = processFile(“C:\\test.txt”);// ok:
} catch (FileNotFoundException e) {// file not found:
} catch (SecurityException e) {// no read permission:
} catch (IOException e) {// io error:
} catch (Exception e) {// other error:
}
因为Java的异常是class
,它的继承关系如下:
┌───────────┐│ Object │└───────────┘▲│┌───────────┐│ Throwable │└───────────┘▲┌─────────┴─────────┐│ │┌───────────┐ ┌───────────┐│ Error │ │ Exception │└───────────┘ └───────────┘▲ ▲┌───────┘ ┌────┴──────────┐│ │ │
┌─────────────────┐ ┌─────────────────┐┌───────────┐
│OutOfMemoryError │... │RuntimeException ││IOException│...
└─────────────────┘ └─────────────────┘└───────────┘▲┌───────────┴─────────────┐│ │┌─────────────────────┐ ┌─────────────────────────┐│NullPointerException │ │IllegalArgumentException │...└─────────────────────┘ └─────────────────────────┘
从继承关系可知:Throwable
是异常体系的根,它继承自Object
。Throwable
有两个体系:Error
和Exception
,Error
表示严重的错误,程序对此一般无能为力,例如:
OutOfMemoryError
:内存耗尽NoClassDefFoundError
:无法加载某个ClassStackOverflowError
:栈溢出
而Exception
则是运行时的错误,它可以被捕获并处理。
捕获异常
捕获异常使用try...catch
语句,把可能发生异常的代码放到try {...}
中,然后使用catch
捕获对应的Exception
及其子类
小结
Java使用异常来表示错误,并通过try ... catch
捕获异常;
Java的异常是class
,并且从Throwable
继承;
Error
是无需捕获的严重错误,Exception
是应该捕获的可处理的错误;
RuntimeException
无需强制捕获,非RuntimeException
(Checked Exception)需强制捕获,或者用throws
声明;
不推荐捕获了异常但不进行任何处理。
捕获异常
多catch语句
可以使用多个catch
语句,每个catch
分别捕获对应的Exception
及其子类。JVM在捕获到异常后,会从上到下匹配catch
语句,匹配到某个catch
后,执行catch
代码块,然后不再继续匹配。
存在多个catch
的时候,catch
的顺序非常重要:子类必须写在前面
抛出异常
异常的传播
当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个try ... catch
被捕获为止
public class Main {public static void main(String[] args) {try {process1();} catch (Exception e) {e.printStackTrace();}}static void process1() {process2();}static void process2() {Integer.parseInt(null); // 会抛出NumberFormatException}
}
NullPointerException
NullPointerException
即空指针异常,俗称NPE。如果一个对象为null
,调用其方法或访问其字段就会产生NullPointerException
,这个异常通常是由JVM抛出的
指针这个概念实际上源自C语言,Java语言中并无指针。我们在Java定义的变量实际上是引用,Null Pointer更确切地说是Null Reference,不过两者区别不大。
处理NullPointerException
如果遇到NullPointerException
,我们应该如何处理?首先,必须明确,NullPointerException
是一种代码逻辑错误,遇到NullPointerException
,遵循原则是早暴露,早修复,严禁使用catch
来隐藏这种编码错误;
使用空字符串""
而不是默认的null
可避免很多NullPointerException
,编写业务逻辑时,用空字符串""
表示未填写比null
安全得多。
返回空字符串""
、空数组而不是null
定位NullPointerException
如果产生了NullPointerException
,例如,调用a.b.c.x()
时产生了NullPointerException
,原因可能是:
a
是null
;a.b
是null
;a.b.c
是null
;
确定到底是哪个对象是null
以前只能打印这样的日志:
System.out.println(a);
System.out.println(a.b);
System.out.println(a.b.c);
从Java 14开始,如果产生了NullPointerException
,JVM可以给出详细的信息告诉我们null
对象到底是谁。
使用断言
断言(Assertion)是一种调试程序的方式。在Java中,使用assert
关键字来实现断言。
我们先看一个例子:
public static void main(String[] args) {double x = Math.abs(-123.45);assert x >= 0;System.out.println(x);
}
语句assert x >= 0;
即为断言,断言条件x >= 0
预期为true
。如果计算结果为false
,则断言失败,抛出AssertionError
。
使用assert
语句时,还可以添加一个可选的断言消息:
assert x >= 0 : "x must >= 0";
这样,断言失败的时候,AssertionError
会带上消息x must >= 0
,更加便于调试。
Java断言的特点是:断言失败时会抛出AssertionError
,导致程序结束退出。因此,断言不能用于可恢复的程序错误,只应该用于开发和测试阶段。
对于可恢复的程序错误,不应该使用断言。例如:
void sort(int[] arr) {assert arr != null;
}
应该抛出异常并在上层捕获:
void sort(int[] arr) {if (arr == null) {throw new IllegalArgumentException("array cannot be null");}
}
当我们在程序中使用assert
时,例如,一个简单的断言:
public class Main {public static void main(String[] args) {int x = -1;assert x > 0;System.out.println(x);}
}
断言x
必须大于0
,实际上x
为-1
,断言肯定失败。执行上述代码,发现程序并未抛出AssertionError
,而是正常打印了x
的值。
这是怎么肥四?为什么assert
语句不起作用?
这是因为JVM默认关闭断言指令,即遇到assert
语句就自动忽略了,不执行。
要执行assert
语句,必须给Java虚拟机传递-enableassertions
(可简写为-ea
)参数启用断言。所以,上述程序必须在命令行下运行才有效果:
$ java -ea Main.java
Exception in thread "main" java.lang.AssertionErrorat Main.main(Main.java:5)
还可以有选择地对特定地类启用断言,命令行参数是:-ea:com.itranswarp.sample.Main
,表示只对com.itranswarp.sample.Main
这个类启用断言。
或者对特定地包启用断言,命令行参数是:-ea:com.itranswarp.sample...
(注意结尾有3个.
),表示对com.itranswarp.sample
这个包启动断言。
实际开发中,很少使用断言。更好的方法是编写单元测试,后续我们会讲解JUnit
的使用。
小结
断言是一种调试方式,断言失败会抛出AssertionError
,只能在开发和测试阶段启用断言;
对可恢复的错误不能使用断言,而应该抛出异常;
断言很少被使用,更好的方法是编写单元测试。
使用JDK Logging
在编写程序的过程中,发现程序运行结果与预期不符,怎么办?当然是用System.out.println()
打印出执行过程中的某些变量,观察每一步的结果与代码逻辑是否符合,然后有针对性地修改代码。
代码改好了怎么办?当然是删除没有用的System.out.println()
语句了。
如果改代码又改出问题怎么办?再加上System.out.println()
。
反复这么搞几次,很快大家就发现使用System.out.println()
非常麻烦。
怎么办?
解决方法是使用日志。
那什么是日志?日志就是Logging,它的目的是为了取代System.out.println()
。
输出日志,而不是用System.out.println()
,有以下几个好处:
- 可以设置输出样式,避免自己每次都写
"ERROR: " + var
; - 可以设置输出级别,禁止某些级别输出。例如,只输出错误日志;
- 可以被重定向到文件,这样可以在程序运行结束后查看日志;
- 可以按包名控制日志级别,只输出某些包打的日志;
- 可以……
总之就是好处很多啦。
那如何使用日志?
因为Java标准库内置了日志包java.util.logging
,我们可以直接用。先看一个简单的例子:
// logging
import java.util.logging.Level;
import java.util.logging.Logger;
public class Hello {public static void main(String[] args) {Logger logger = Logger.getGlobal();logger.info("start process...");logger.warning("memory is running out...");logger.fine("ignored.");logger.severe("process will be terminated...");}
}
运行上述代码,得到类似如下的输出:
Mar 02, 2019 6:32:13 PM Hello main
INFO: start process...
Mar 02, 2019 6:32:13 PM Hello main
WARNING: memory is running out...
Mar 02, 2019 6:32:13 PM Hello main
SEVERE: process will be terminated...
对比可见,使用日志最大的好处是,它自动打印了时间、调用类、调用方法等很多有用的信息。
再仔细观察发现,4条日志,只打印了3条,logger.fine()
没有打印。这是因为,日志的输出可以设定级别。JDK的Logging定义了7个日志级别,从严重到普通:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
因为默认级别是INFO,因此,INFO级别以下的日志,不会被打印出来。使用日志级别的好处在于,调整级别,就可以屏蔽掉很多调试相关的日志输出。
反射
什么是反射?
反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。
正常情况下,如果我们 要调用 一个对象 的方法,或者访问一个 对象的字段 , 通常会 传入 对象实例:
// Main.java
import com.itranswarp.learnjava.Person;public class Main {String getFullName(Person p) {//参数就是传入 对象实例return p.getFirstName() + " " + p.getLastName();}
}
上面代码中,要调用对象p的方法,就得 传入 Person p 对象 实例,
但是,如果 不能获得Person
类, 只有一个Object
实例,比如这样:
String getFullName(Object obj) {return ???
}
怎么办?---怎样才能调用到Person类的对象的方法???运行时怎么获取person类的所有信息)
有童鞋会说:强制转型啊!
String getFullName(Object obj) {Person p = (Person) obj; //强制转型return p.getFirstName() + " " + p.getLastName();
}
强制转型的时候,你会发现一个问题:编译上面的代码,仍然需要引用Person
类。不然,去掉import
语句,你看能不能编译通过?-------相当于获得Person类的对象,再调用其方法,这就与我们原本的假设 :不能获得Person类,相矛盾。 ----那么,这就 与 反射 相背,下面说了,反射是在对 某个实例 一无所知的情况下,,,实现调用其方法!
所以,反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。
lxf-java selected study相关推荐
- java 图形化库_java图形界面之图形化按钮
要将按钮图形化,只需创建一个ImageIcon对象,将图形路径赋予ImageIcon对象,然后将该对象传递给按钮即可. 此处涉及eclipse中图形的路径设置,包括(项目路径下.非项目路径下.相对路径 ...
- Java中的main()方法详解
源文作者:leizhimin 源文链接:http://lavasoft.blog.51cto.com/62575/53263 源文作者版权申明: 版权声明:原创作品,允许转载,转载时请务必以超链 ...
- 中科院分词系统(NLPIR)JAVA简易教程
这篇文档内容主要从官方文档中获取而来,对API进行了翻译,并依据个人使用经验进行了一些补充,鉴于能力有限.水平较低,文档中肯定存在有很多不足之处,还请见谅. 下载地址:http://ictclas.n ...
- Python自动生成10000个java类使用APT注解后引发的问题
前言 前面写了一篇关于自己开发的一个基于APT注解的用于RecyclerView复杂楼层的开源框架,框架的原理比较简单,通过注解,在编译期会生成一个ComponentRule.java的文件,然后建立 ...
- Java宣言的时候,Java基础恶补——宣言及访问控制
Java基础恶补--声明及访问控制 [SCJP Sun Certified Programmer for Java 6 Study Guide (Exam 310-065)] chapter 1 一 ...
- java APIs for database -------- JDBC (1)connection
# JDBC -- The Java™ Tutorials # Study Note of JDBC JDBC Study Note ----connect to database 通常,使用JDBC ...
- Java学习——Day13:IO流
6.1 File类 java.io.File类:文件和文件目录路径的抽象表示形式,与平台无关 File 能新建.删除.重命名文件和目录,但 File 不能访问文件内容本身. 如果需要访问文件内容本身, ...
- 网易云课堂微专业--Java高级开发工程师
获取方式:公众号:文若课堂 5.微专业-java | |──大纲.jpg 910.32KB | |──阶段1:高性能编程专题 | | |──1.1.1 J ...
- java 魔方_2 java实现4阶魔方,通过运行代码,鼠标进行旋转,可以模拟 游戏 Develop 246万源代码下载- www.pudn.com...
开发工具: Java 文件大小: 3830 KB 上传时间: 2015-11-07 下载次数: 0 详细说明:java实现4阶魔方,通过运行代码,鼠标进行旋转,可以模拟魔方游戏-java achiev ...
最新文章
- Nucleus SE RTOS初始化和启动
- CSS3概述、选择器、兼容性、样式
- 基本Linux命令的用法
- mysql表索引类型修改_MySQL常用的建表、添加字段、修改字段、添加索引SQL语句写法总结...
- 使用百度地图实现基本的地图显示与定位功能
- 迷瘴 详解(C++)
- 德歌:阿里云RDS PG最佳实践
- MacOS 显示隐藏文件快捷键
- C#项目获取当前时间的农历时间
- 相机模型-鱼眼模型/Omnidirectional Camera(1)
- 永久免费!永洪科技发布桌面智能数据分析工具Desktop,推动数据应用平民化
- RMAN crosscheck archivelog all; 失败!
- Mysql数据库报错1264
- GHM(anchor based)
- node和npm的关系,node和npm的关联
- 短视频头部效应加剧,秒拍整军三月强势归来
- Head Above Water
- 哈佛经典:领导者应该做什么?(领导与管理的不同)
- 问渠哪得清如许,唯有源头活水来-浅谈android 系统
- WIN2000终端网络简介(转)