面向对象
Java 使用面向对象编程(Object-Oriented Programming,OOP),这是一种编程风格,它旨在使思考编程更接近思考现实世界。
java 中的对象
在面向对象中,每个对象都是一个独立的单位,就像现实世界中的对象一样。
看看周围真实的世界,会发现身边有很多对象,车,猫,人等等。所有这些对象都有自己的状态和行为。
拿一只猫来举例,它的状态有:猫名、猫龄、颜色,行为有:睡觉、吃食和发出声音。
对比现实对象和软件对象,它们之间十分相似。
软件对象也有状态和行为。软件对象的状态就是属性,行为通过方法体现。
在软件开发中,方法操作对象内部状态的改变,对象的相互调用也是通过方法来完成。
Java 中的类
一个类描述对象将是什么,但是与对象本身是分开的。
换句话说,类可以描述为对象的模板,描述或定义。
您可以使用相同的类作为创建多个对象的模板。第一步是定义类,然后成为对象创建的模板。
每个类都有一个名称,每个类都用于定义属性和行为。
下面通过一个简单的类来理解Java中类的定义:
public class Cat{
String name;
int age;
String color;
void sleeping(){
}
void eating(){
}
void barking(){
}
}
方法
在 Java 中,方法定义行为。一个方法是一个组合在一起执行操作的语句的集合。System.out.println() 是一个方法的例子。
您可以定义自己的方法来执行所需的任务。
下面是一个例子:
class MyJavaClass {
static void sayHello() {
System.out.println("Hello Edong!");
}
public static void main(String[] args) {
sayHello();
}
}
// 输出 "Hello Edong!"
在上面的例子中,声明了一个名为 “sayHello” 的方法,它会输出一行文本,然后在 main 方法中调用。
提示:若要调用一个方法,请输入其名称,然后在名称后面加上一组圆括号。
方法调用
在 Java 中,您可以根据需要多次调用方法。
当一个方法运行时,代码会跳转到方法定义的位置,执行它内部的代码,然后返回到下一行。
下面是一个例子:
class MyJavaClass {
static void sayHello() {
System.out.println("Hello Edong!");
}
public static void main(String[] args) {
sayHello();
sayHello();
sayHello();
}
}
// Hello Edong!
// Hello Edong!
// Hello Edong!
方法参数
您也可以创建一个方法,在调用它时将一些数据称为参数。在方法的括号内写入参数。
例如,我们可以修改我们的 sayHello() 方法来获取并输出一个字符串参数。
class MyJavaClass {
static void sayHello(String name) {
System.out.println("Hello " + name);
}
public static void main(String[] args) {
sayHello("Loen");
sayHello("Shuter");
}
}
// Hello Loen
// Hello Shuter
上面的方法将一个名为 name 的字符串作为参数,用在方法的主体中。
然后,在调用方法时,我们将参数的值传递给括号。
提示:在 Java 中,方法可以采用多个以逗号分隔的参数。
返回类型
返回关键字可用于方法返回值。
例如,我们可以定义一个名为sum的方法,返回两个参数的总和。
static int sum(int val1, int val2) {
return val1 + val2;
}
在方法定义中,我们在定义方法名称之前定义了返回类型。对于sum方法,它是int类型,因为它采用int类型的两个参数,并返回它们的总和,这也是一个int类型。
提示:关于static关键字的知识,我们将在后面的小节中去了解学习。
现在,我们可以使用我们main的方法。
class MyJavaClass {
static int sum(int val1, int val2) {
return val1 + val2;
}
public static void main(String[] args) {
int x = sum(3, 6);
System.out.println(x);
}
}
// 输出 "9"
当该方法返回一个值时,我们可以将其赋值给一个变量。
提示:
如果不需要从方法中返回任何值,请使用关键字void。
注意main方法定义中的void关键字,这意味着main不返回任何内容。
返回类型
通过前面小节的学习介绍,我们可以更好地理解程序是如何工作的:
// 返回一个int值 6
static int returnSix() {
return 6;
}
// 输出传入的参数
static void sayHelloTo(String name) {
System.out.println("Hello " + name);
}
// 打印 "Hello Lu!"
static void sayHello() {
System.out.println("Hello Lu!");
}
在了解方法返回类型和参数的知识之后,我们再来看看main方法的定义。
public static void main(String[] args)
这个定义表明main方法以一个字符串数组作为参数,并且不返回任何值。
让我们创建一个方法,它采用int类型的两个参数,并返回这两个参数的最大值,然后在main中调用它:
public static void main(String[] args) {
int result = max(5, 35);
System.out.println(result); // 35
}
static int max(int x, int y) {
if(x > y) {
return x;
}
else {
return y;
}
}
创建类
为了创建自己的自定义对象,你必须先创建相应的类。
你可以使用Eclipse开发工具,通过右键点击src目录,并选择 New(新建)-> Class(类)。
给你的类提供一个名称,然后点击 “Finish(完成)” 将新类添加到项目中。结果如下图所示:
如你所见,Eclipse已经为这个类添加了初始代码。
现在,让我们在新类中创建一个简单的方法。
Animal.java 文件代码:
public class Animal {
void barking() {
System.out.println("Meow-Meow");
}
}
在上面的代码中,我们在Animal类中声明了一个barking()方法。
提示:为了使用类和它的方法,我们需要声明这个类的一个对象。
创建对象
对象是根据类创建的。在Java中,使用关键字new来创建一个新的对象。
下面是一个创建对象的例子:
class AnimalDemo {
public static void main(String[] args) {
Animal cat = new Animal();
cat.barking();
}
}
// 输出 "Meow-Meow"
在上面的例子中,cat 是 Animal 的对象。因此,我们可以使用对象的名称和点来调用它的barking()方法。
提示:点符号用于访问对象的属性和方法。
定义属性
一个类有属性和方法。属性基本上是一个类中的变量。
让我们创建一个名为 Dog 的类,它具有相应的属性和方法。
public class Dog {
String name;
int age;
String color;
void barking() {
System.out.println("Woof-Woof");
}
}
在上面的例子中,name,age,和color是Dog类的属性,而barking()是唯一的方法。
提示:你可以根据需要定义多个属性和方法。
创建对象
接下来,我们可以创建Dog类的多个对象,并使用点符号来访问它们的属性和方法。
下面是一个例子:
class DogDemo {
public static void main(String[] args) {
Dog d1 = new Dog();
Dog d2 = new Dog();
d1.color = "white";
d2.barking();
}
}
访问修饰符
现在,让我们来了解main方法前面的 public 关键字。
public static void main(String[] args)
public 是一个访问修饰符,它被用来设置访问级别。在 Java 中,可以使用访问修饰符来保护对类、变量、方法的访问。
Java 支持 4 种不同的访问权限。
default (即缺省,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
private : 在同一类内可见。使用对象:变量、方法。
public : 对所有类可见。使用对象:类、接口、变量、方法
protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。
下面是一个例子:
public class Dog {
private String name;
private int age;
private String color;
public void barking() {
System.out.println("Woof-Woof");
}
}
注意: private 和 protected 修饰符不能修饰类(外部类)。
getter和setter方法
声明为私有访问类型的变量只能通过类中公共的getter/setter方法被外部类访问。
getter和setter方法主要用来隐藏类的实现细节和保护类的数据。
对于每个变量,get方法返回其值,而set方法设置该值。
getter方法以get开头,setter方法以set开始,后面都跟着变量名,变量名的第一个字母大写。
下面是一个例子:
public class Dog {
private String color;
// getter
public String getColor() {
return color;
}
// setter
public void setColor(String c) {
this.color = c;
}
}
getter方法返回属性的值。setter方法接受一个参数并将其赋值给属性。
提示:关键字this用于引用当前对象。也就是说,this.color是当前对象的颜色属性。
getter和setter方法
一旦我们的getter和setter被定义了,我们可以在我们的main中使用它。
下面是一个例子:
public static void main(String[] args) {
Dog d1 = new Dog();
d1.setColor("White");
System.out.println(d1.getColor());
}
//输出 "White"
getter和setter允许我们控制这些值。例如,您可以在实际设置该值之前在设置器中验证给定的值。
提示:getter和setter是封装的基本构建块,你将在后面的章节中去了解学习。
构造函数
构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值。
可以使用构造函数为对象属性提供初始值。
构造函数名称必须与其类名相同。
构造函数不能有明确的返回类型。
下面是一个构造函数的例子:
public class Dog {
private String color;
Dog() {
color = "White";
}
}
Dog()方法是我们类的构造函数,所以无论何时创建该类的对象,color属性都将设置为“White”。
构造函数也可以带参数来初始化属性。
例如:
public class Dog {
private String color;
Dog(String c) {
color = c;
}
}
使用构造函数
new对象时,就调用与之对应的构造函数。
下面是一个例子:
public class MyJavaClass {
public static void main(String[] args) {
Dog d = new Dog("Yellow");
}
}
这将调用构造函数,将设置color属性为“Yellow”。
注:一般函数不能调用构造函数,只有构造函数才能调用构造函数。
构造函数
一个类可以有多个构造函数,它们具有不同数量的参数。
构造函数中的setter方法可以用来设置属性值。
下面是一个例子:
public class Dog {
private String color;
Dog() {
this.setColor("White");
}
Dog(String c) {
this.setColor(c);
}
// setter
public void setColor(String c) {
this.color = c;
}
}
上面的类有两个构造函数,一个没有任何参数,将color属性设置为默认值“White”,另一个构造函数接受一个参数并将其赋值给属性。
现在,我们可以使用构造函数来创建我们的类的对象。
//颜色将是"White"
Dog d1 = new Dog();
//颜色将是"Yellow"
Dog d2 = new Dog("Yellow");
提示:Java 会自动提供一个默认的构造函数,所以所有的类都有一个构造函数,无论是否有特殊的定义。
值类型
基本类型 包括 byte,short,int,long,float,double,boolean和char。
Java方法参数传递的到底是值还是引用?
基本类型传的是变量的值。
所以,当你将它们传递给一个方法时,你基本上是对变量的值进行操作,而不是对变量本身进行操作。
最常见的说法是基本类型传的是值,对象传的引用。
下面是一个例子:
public class MyJavaClass {
public static void main(String[] args) {
int x = 10;
addTo(x); // 这时候等价于 addTo(10)
System.out.println(x);
}
static void addTo(int num) {
num = num + 1;
}
}
// 输出 "10"
在上面的例子中,方法取其参数的值,这就是为什么原始变量x不受影响,10仍然是x的值。
引用类型
引用类型传递的是对象地址值,而非对象或副本。
当你使用构造函数创建对象时,你将创建一个引用变量。
例如,我们要定义一个Student类:
public class MyJavaClass {
public static void main(String[] args) {
Student j;
j = new Student("Jing");
j.setAge(18);
addTo(j);
System.out.println(j.getAge());
}
static void addTo(Student s) {
s.setAge(s.getAge() + 1);
}
}
//输出 "19"
addTo方法将Student对象作为参数,并增加其属性。
由于 j 是引用类型,因此该方法影响对象本身,并且可以更改其属性的实际值。
提示:数组和字符串也是引用类型。
Math 类
JDK定义了许多有用的类,其中一个是Math类,它为数学运算提供了预定义的方法。
你不需要创建Math类的对象来使用它。要访问它,只需输入Math. 和相应的方法。
Math.abs() 返回参数的绝对值。参数可以是 int, float, long, double, short, byte类型。
int a = Math.abs(5); // 5
int b = Math.abs(-10); // 10
Math.ceil() 将一个数进行上舍入,返回值大于或等于给定的参数。
double c = Math.ceil(6.231); // 7.0
Math.floor() 将对一个数进行下舍入,返回给定参数最大的整数,该整数小于或等给定的参数。
double f = Math.floor(6.231); // 6.0
Math.max() 用于返回两个参数中的最大值。
int m = Math.max(5,10); // 10
相反,Math.min() 用于返回两个参数中的最小值。
int m = Math.min(5,10); // 5
Math.pow() 用于返回第一个参数的第二个参数次方。值以double形式返回。
double p = Math.pow(2,3); // 8.0
提示:Math 类还提供了其他一些方法,例如:sqrt(),sin(),cos() 方法等等。
静态变量
当你声明一个变量或方法为静态时,它属于这个类,而不是一个特定的实例。
不管你创建了该类的多个对象,或者如果你不创建任何对象, 有且只有一个静态成员的实例存在。
该静态成员将被所有对象共享。
static 关键字用来声明独立于对象的静态变量。
下面是一个例子:
public class Counter {
public static int numCount = 0;
Counter() {
numCount++;
}
}
numCount 变量将被该类的所有对象共享。
现在,我们可以在main中创建Counter类的对象,并访问静态变量。
public class MyJavaClass {
public static void main(String[] args) {
Counter c1 = new Counter();
Counter c2 = new Counter();
System.out.println(Counter.numCount);
}
}
//输出 "2"
上面代码的输出结果是2,因为numCount变量是静态的,每次创建一个Counter类的新对象时都会增加1,而在代码中,我们创建了2个对象。
你也可以使用该类的任何对象(如c1.numCount)来访问静态变量。
静态方法
相同的概念也适用于静态方法。static 关键字用来声明独立于对象的静态方法。
下面是一个例子:
public class Dog {
public static void barking() {
System.out.println("Woof-Woof");
}
}
现在,可以在不创建对象的情况下调用barking方法:
public class MyJavaClass {
public static void main(String[] args) {
Dog.barking();
}
}
静态方法的另一个例子是Math类,这就是为什么你可以在不创建Math对象的情况下调用它们的原因。
提示:main方法必须始终是静态的。
final
使用 final 关键字标记一个常量,常量只能被赋值一次。
final 修饰符通常和 static 修饰符一起使用来创建类常量。
下面是一个例子:
class MyJavaClass {
public static final double PI = 3.14;
public static void main(String[] args) {
System.out.println(PI);
}
}
PI 现在是一个常量。任何为其赋值的尝试都会输出错误。
提示:方法和类也可以标记为 final,这样做可以限制方法,使其不能被重写,并且不能使其成为子类。在后面的章节中,我们将进一步的介绍有关知识。
包(package)
Java 使用包(package)这种机制是为了防止命名冲突,访问控制,提供搜索和定位类、接口、枚举和注释等。
一个包(package)可以定义为一组相互联系的类型(类、接口、枚举和注释),为这些类型提供访问保护和命名空间管理的功能。
用Java创建一个包是非常简单的。使用Eclipse开发工具,通过右键点击项目中的src目录,并选择 New(新建)-> Package(包)。为这个包取一个合适的名字(例如:w3cschool),然后点击 “Finish(完成)” 。
你会注意到新的包出现在项目目录中。现在你可以在这个包里面创建和移动类。结果如下图所示:
在包中创建/移动类时,以下代码将出现在文件列表的顶部。
package w3cschool;
这表示这个类所属的包。
现在,我们需要导入包中的类,以便能够使用它们。
下面是一个例子,显示如何使用w3cschool包的Cat类。
import w3cschool.Cat;
class MyJavaClass {
public static void main(String[] args) {
Cat c1 = new Cat();
c1.barking();
}
}
当一个类被放入一个包中时会发生两个主要的结果。首先,包的名称成为该类名称的一部分。其次,包的名称必须与相应的类文件所在的目录结构相匹配。
提示:使用通配符 "" 导入包中的所有类。例如,import w3cschool. 将导入w3cschool包中的所有类。
封装
面向对象编程(OOP)的主要特征是:封装,继承,多态。
封装背后的想法是确保实现细节对用户不可见。
一个类的变量将被其他类隐藏,只能通过当前类的方法访问。这被称为数据隐藏。
为了在Java中实现封装,将类的变量声明为private,并提供public getter和setter方法来修改和查看变量的值。
下面是一个例子:
class BankAccount {
private double balance = 0;
public void deposit(double x) {
if(x > 0) {
balance += x;
}
}
}
这段代码中,实现隐藏了balance变量,只能通过deposit方法来访问它,在修改变量之前验证了要存入的金额。
总之,封装提供了以下优点:
良好的封装能够减少耦合。
类内部的结构可以自由修改。
可以对成员变量进行更精确的控制。
隐藏信息,实现细节。
继承
继承是使一个类获得另一个类的属性(方法和变量)的过程。通过继承,信息被放置在更易于管理的层次上。
继承另一个属性的类称为子类(也称为派生类)。其属性被继承的类称为父类(基类或超类)。
在 Java 中,通过 extends 关键字可以申明一个类是从另外一个类继承而来的。
下面是一个例子,展示了如何让类 Cat 继承 Animal 类。
class Cat extends Animal {
// 其余代码
}
在这里,Cat 是子类,Animal 是父类。
当一个类从另一个类继承时,它继承了所有父类的非私有变量和方法。
下面是一个例子:
class Animal {
protected int legs;
public void eat() {
System.out.println("Animal eats");
}
}
class Cat extends Animal {
Cat() {
legs = 4;
}
}
如你所见,Cat 类继承了 Animal 类中的 legs 变量。
现在,我们可以声明一个 Cat 对象,并调用它的父类的 eat 方法:
class MyJavaClass {
public static void main(String[] args) {
Cat c = new Cat();
c.eat();
}
}
提示:protected 访问修饰符,protected 使成员只对子类可见。
构造函数不是成员方法,所以不会被子类继承。
然而,父类的构造函数在子类被实例化时会被调用。
下面是一个例子:
class A {
public A() {
System.out.println("New A");
}
}
class B extends A {
public B() {
System.out.println("New B");
}
}
class Test {
public static void main(String[] args) {
B obj = new B();
}
}
/*输出
"New A"
"New B"
*/
提示:我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。例如,super.var 将访问父类的var成员。
多态
多态是同一个行为具有多个不同表现形式或形态的能力。多态性是对象多种表现形式的体现。
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。
多态的优点
- 消除类型之间的耦合关系
- 可替换性
- 可扩充性
- 接口性
- 灵活性
- 简化性
多态存在的三个必要条件
继承
重写
父类引用指向子类对象
下面是一个例子,Cat 和 Dog 是继承 Animal 类的类。每个类都有自己的 barking() 方法的实现。
class Animal {
public void barking() {
System.out.println("Hi");
}
}
class Cat extends Animal {
public void barking() {
System.out.println("Meow-Meow");
}
}
class Dog extends Animal {
public void barking() {
System.out.println("Woof-Woof");
}
}
由于所有 Cat 和 Dog 对象都是 Animal 对象,我们可以主要执行以下操作:
public static void main(String[] args) {
Animal a = new Dog();
Animal b = new Cat();
}
我们创建了两个类型为 Animal 的引用变量,并将它们指向 Cat 和 Dog 对象。
现在,我们可以调用 barking() 方法。
a.barking();
//输出 "Woof-Woof"
b.barking();
//输出 "Meow-Meow"
由于引用变量 a 指向 Dog 对象,Dog 类的 barking() 方法将被调用。这同样适用于 b 变量。
提示:这表明,你可以使用Animal变量而不需要知道它包含了子类的一个对象。当你有父类的多个子类时,这是非常有用的。
方法重写
重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写。
重写的好处在于子类可以根据需要,定义特定于自己的行为。也就是说子类能够根据需要实现父类的方法。
下面是一个例子:
class Animal {
public void barking() {
System.out.println("Hi");
}
}
class Dog extends Animal {
public void barking() {
System.out.println("Woof-Woof");
}
}
在上面的代码中,Dog 类重写了其父类 Animal 的 barking() 方法。
方法的主要重写规则:
必须有相同的参数列表和返回类型。
访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected。
声明为final或static的方法不能被重写。
构造方法不能被重写。
如果不能继承一个方法,则不能重写这个方法。
方法重载
重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。
每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
最常用的地方就是构造器的重载。
重载规则:
被重载的方法必须改变参数列表(参数个数或类型不一样);
被重载的方法可以改变返回类型;
被重载的方法可以改变访问修饰符;
被重载的方法可以声明新的或更广的检查异常;
方法能够在同一个类中或者在一个子类中被重载。
无法以返回值类型作为重载函数的区分标准。
例子:
public class Overloading {
public int test(){
System.out.println("test1");
return 1;
}
public void test(int a){
System.out.println("test2");
}
//以下两个参数类型顺序不同
public String test(int a,String s){
System.out.println("test3");
return "returntest3";
}
public String test(String s,int a){
System.out.println("test4");
return "returntest4";
}
public static void main(String[] args){
Overloading o = new Overloading();
System.out.println(o.test());
o.test(1);
System.out.println(o.test(1,"test3"));
System.out.println(o.test("test4",1));
}
}
提示:被重载的方法必须改变参数列表(参数个数或类型或顺序不一样)。
重写与重载之间的区别
区别点 重载方法 重写方法
参数列表 必须修改 一定不能修改
返回类型 可以修改 一定不能修改
异常 可以修改 可以减少或删除,一定不能抛出新的或者更广的异常
访问 可以修改 一定不能做更严格的限制(可以降低限制)
总结
方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。
(1)方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
(2)方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
(3)方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
抽象概念
数据抽象为外界提供了仅有的基本信息,在表示基本特征的过程中不包括实现细节。
举个真实世界的例子,比如一本书。当你听到是书时,你不知道具体的细节,如页数,颜色或大小,但你明白书的概念、大概模样。这就是对书的抽象。
抽象的概念是我们关注基本特征,而不是一个特定例子的具体特征。
在 Java 中,抽象是使用抽象类和接口实现的。
抽象类是使用 abstract 关键字定义的。
如果一个类声明为抽象类,则不能被实例化(不能创建该类型的对象)。
要使用抽象类,必须从另一个类继承它。
抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
提示:抽象方法没有定义,方法名后面直接跟一个分号,而不是花括号。如:abstract void run();
抽象类
例如,我们可以将 Animal 类定义为抽象类:
abstract class Animal {
int legs = 0;
abstract void barking();
}
barking 方法也是抽象的,因为它在父类中没有实现。
我们可以继承 Animal 类并为子类定义 barking() 方法:
class Dog extends Animal {
public void barking() {
System.out.println("Woof-Woof");
}
}
每种动物都会发出声音,但每种动物都有不同的叫声。这就是我们为什么要定义一个抽象类 Animal 的原因,并且把它们如何发出声音的实现留给子类。
接口
一个接口是一个完全抽象的类,只包含抽象方法。接口通常以interface来声明。
接口的一些规则:
类里面可以声明 public static final 修饰的变量。
接口不能被实例化,但是可以被实现类创建。
一个类可以实现多个接口。
一个接口能继承另一个接口,这和类之间的继承比较相似。
下面是一个简单的接口例子:
interface Animal {
public void eating();
public void barking();
}
接口有以下特性:
接口是隐式抽象的,当声明一个接口的时候,不必使用abstract关键字。
接口中每一个方法也是隐式抽象的,声明时同样不需要abstract关键字。
接口中的方法都是公有的。
提示:一个类只能继承一个类,但是能实现多个接口。
接口的实现
当类实现接口的时候,类要实现接口中所有的方法。否则,类必须声明为抽象的类。
类使用 implements 关键字实现接口。在类声明中,Implements 关键字放在 class 声明后面。
下面是一个例子:
interface Animal {
public void eating();
public void barking();
}
class Dog implements Animal {
public void barking() {
System.out.println("Woof-Woof");
}
public void eating() {
System.out.println("crunch-crunch");
}
}
提示:当你实现一个接口时,你需要重写所有的方法。
类型转换
将一个类型的值赋给另一个类型的变量称为类型转换。
要将值转换为特定类型,请将该类型放在括号中,并将其放在值的前面。
下面是一个例子:
int x = (int)5.25;
System.out.println(x);
//输出 5
上面的代码中,将值5.25转换为一个整数,输出5为结果值。
再来看另外一个例子:
double x = 27.582;
int y = (int)x;
System.out.println(y);
//输出 27
提示:Java 支持将整数自动类型转换为浮点,因为没有精度损失。另外,在将浮点值赋值给整型变量时,强制类型转换是强制性的, 可能会有精度损失。
向上转型和向下转型
对于类,有两种类型的转换。
向上转型(Upcasting)
您可以将一个子类的实例转换为其父类。这是向上转换。
下面是一个例子,比如 Dog 是 Animal 的一个子类。
Animal a = new Dog();
Java 自动将 Dog 类型的变量上传到 Animal 类型。
向下转型(Downcasting)
将父类的对象转换为其子类称为向下转换。
下面是一个例子:
((Dog)a).barking();
上面的代码中,尝试将变量 a 转换为 Dog 类型并调用其 barking() 方法。
匿名类
匿名类是一种即时继承现有类的方法。
例如,下面有一个类 Computer:
class Computer {
public void start() {
System.out.println("Starting");
}
}
创建 Computer 对象时,我们可以即时更改 start 方法。
public static void main(String[] args) {
Computer c = new Computer() {
@Override public void start() {
System.out.println("Wooooo");
}
};
c.start();
}
// 输出 "Wooooo";
在构造函数调用之后,我们使用花括号,并且重写了 start 方法的实现。
提示:@Override注解用于使代码更容易理解,因为当方法被重写时,它更加明显。
修改只适用于当前对象,而不适用于类本身。所以,如果我们创建这个类的另一个对象,那么 start 方法的实现就是在类中定义的那个。
class Computer {
public void start() {
System.out.println("Starting");
}
}
public static void main(String[] args) {
Computer c1 = new Computer() {
@Override public void start() {
System.out.println("Wooooo");
}
};
Computer c2 = new Computer();
c2.start();
}
//输出 "Starting"
内部类
Java 支持嵌套类,一个类可以是另一个类的成员。
创建一个内部类很简单,只要在类中编写一个类。
作用:
实现了更好的封装,我们知道,普通类(非内部类)的访问修饰符不能为private或protected,而内部类可以。当我们将内部类声明为private时,只有外部类可以访问内部类,很好地隐藏了内部类。
内部类可以继承(extends)或实现(implements)其他的类或接口,而不受外部类的影响。
内部类可以直接访问外部类的字段和方法,即使是用private修饰的,相反的,外部类不能直接访问内部类的成员。
下面是一个例子:
class Robot {
int id;
Robot(int i) {
id = i;
Brain b = new Brain();
b.think();
}
private class Brain {
public void think() {
System.out.println(id + " is thinking");
}
}
}
提示:类Robot有一个内部类Brain。内部类可以访问外部类的所有成员变量和方法,外部类不能直接访问内部类的成员。
比较对象
请记住,当你创建对象时,变量存储的是对象的引用。
所以,当使用(==)比较对象时,它实际上比较的是引用而不是对象值。
下面是一个例子:
class Animal {
String name;
Animal(String n) {
name = n;
}
}
class MyJavaClass {
public static void main(String[] args) {
Animal a1 = new Animal("Kitty");
Animal a2 = new Animal("Kitty");
System.out.println(a1 == a2);
}
}
//输出 false
提示:尽管有两个名称相同的对象,因为我们有两个不同的对象(两个不同的引用或内存位置),所以相等性测试返回 false。
equals()
每个对象都有一个预定义的equals()方法,用于语义相等性测试。
但是,为了使它适用于我们的类,我们需要重写它,并检查我们需要的条件。
有一个简单而快速的方法来生成 equals() 方法,你不需要手动编写。
你只需右键单击你的类,选择 Source -> Generate hashCode() and equals()…来完成,如下图所示:
这将自动创建必要的方法。如下面例子所示:
class Animal {
String name;
Animal(String n) {
name = n;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Animal other = (Animal) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
自动生成的hashCode()方法用于确定内部存储对象的位置。每当你实现equals时,你也必须实现hashCode。
我们可以使用equals方法再次运行测试:
public static void main(String[] args) {
Animal a1 = new Animal("Kitty");
Animal a2 = new Animal("Kitty");
System.out.println(a1.equals(a2));
}
//输出 true
提示:你可以使用相同的操作来生成其他有用的方法,例如:类属性的getter和setter方法。
枚举
枚举是Java1.5引入的新特性,通过关键字 enum 来定义枚举类。
枚举类是一种特殊类,它和普通类一样可以使用构造器、定义成员变量和方法,也能实现一个或多个接口,但枚举类不能继承其他类.
枚举类型使用的最常用类型就是枚举常量。
下面是一个简单的枚举例子:
enum Rank {
BRONZE,
SILVER,
GOLD
}
请注意,这些值是用逗号分隔的。
你可以使用点符号在上面的枚举中引用常量。
Rank a = Rank.BRONZE;
提示:枚举定义了代表固定集成员的变量。
声明一个枚举之后,我们可以检查相应的值,例如,一个switch语句。
Rank a = Rank.BRONZE;
switch(a) {
case BRONZE:
System.out.println("第三名!");
break;
case SILVER:
System.out.println("第二名!");
break;
case GOLD:
System.out.println("第一名!");
break;
}
//输出 "第三名!"
当变量(特别是方法参数)只能从一小组可能的值中取出一个时,你应该总是使用枚举。
提示:枚举的使用示例,包括月份,星期几,颜色、学历、职业等。
Java API
Java API是为你编写的类和接口的集合。
包含所有可用API的Java API文档可以在Oracle网站上找到 (http://docs.oracle.com/javase/7/docs/api/)。
当找到想要使用的软件包,我们需要将其导入到代码中。使用import关键字导入。
下面是一个例子:
import java.awt.*;
awt 包中包含了用于创建用户界面和绘制图形图像的所有类。
提示:通配符(*)用于导入包中的所有类。
异常
程序执行过程中出现异常。异常会导致程序异常终止。
异常处理是处理运行时错误以保持正常应用程序流的强大机制。
异常发生的原因有很多,通常包含以下几大类:
用户输入了非法数据。
要打开的文件不存在。
网络通信时连接中断,或者JVM内存溢出。
提示:如你所见,这些异常有的是因为用户错误引起,有的是程序错误或物理错误引起的。但是,一个写得好的程序应该处理所有可能的异常情况。
异常处理
使用 try 和 catch 关键字可以捕获异常。
try/catch 代码块放在异常可能发生的地方。try/catch 代码块中的代码称为保护代码。
使用 try/catch 的语法如下:
try {
//程序代码
} catch(Exception e) {
//Catch 块
}
Catch 语句包含要捕获异常类型的声明。当 try 块中发生一个异常时,try 后面的 catch 块就会被检查。如果发生的异常包含在 catch 块中,异常会被传递到该 catch 块,这和传递一个参数到方法是一样。
Exception 类型可以用来捕获所有可能的异常。
下面的例子中,说明了当尝试访问一个不存在的数组索引时的异常处理:
public class ExcepTest{
public static void main(String[] args) {
try {
int a[] = new int[2];
System.out.println(a[3]);
} catch (Exception e) {
System.out.println("An error occurred");
}
}
}
//输出 "An error occurred"
没有 try/catch 块,这段代码会导致程序崩溃,因为[3]不存在。
注意:catch 块中的(Exception e)语句,它用于捕获所有可能的异常。
throw
使用 throw 关键字可以手动生成方法中的异常。一些可用的异常类型包括 IndexOutOfBoundsException (非法索引访问数组时抛出的异常),IllegalArgumentException(非法参数异常),ArithmeticException(出现异常的运算条件时,抛出此异常) 等等。
例如,当参数为0时,我们可以在方法中抛出 ArithmeticException。
int div(int a, int b) throws ArithmeticException {
if(b == 0) {
throw new ArithmeticException("Division by Zero");
} else {
return a / b;
}
}
方法定义中的throws语句定义了方法可以抛出的Exception的类型。
接下来,throw关键字会引发相应的异常,并附带自定义消息。
如果我们调用第二个参数等于0的div方法,它将抛出一个ArithmeticException,并带有“Zero by Division”消息。
提示:可以使用逗号分隔列表在 throws 语句中定义多个异常。
异常处理
一个 try 代码块后面可以跟随多个 catch 代码块,这种情况就叫多重捕获,分别处理不同的异常。
多重捕获的语法如下所示:
try {
// 程序代码
} catch (异常类型1 异常的变量名1) {
// 程序代码
} catch (异常类型2 异常的变量名2) {
// 程序代码
} catch (异常类型3 异常的变量名3) {
// 程序代码
}
提示:所有 catch 块应该从最具体到最普遍的顺序排列。在特定的异常之后,您可以使用Exception类型来处理所有其他异常作为最后一个catch。
多线程编程
Java是一种多线程编程语言。我们的程序可以通过同时运行两个或多个组件来优化使用可用资源,每个组件处理不同的任务。
你可以将单个应用程序中的特定操作细分为全部并发运行的单个线程。
线程是一个动态执行的过程,它也有一个从出生到死亡的过程。下图显示了一个线程完整的生命周期。
Java 提供了两种创建线程的方法。
创建一个线程的第一种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。
继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。
下面是一个例子:
class Loader extends Thread {
public void run() {
System.out.println("Hello Lu");
}
}
class MyJavaClass {
public static void main(String[] args) {
Loader obj = new Loader();
obj.start();
}
}
如你所见,我们的Loader类继承了Thread类,并重写了它的run()方法。
当我们创建obj对象并调用它的start()方法时,run()方法语句在另一个线程上执行。
提示:每个Java线程都被赋予优先级,以帮助操作系统确定线程调度的顺序。优先级范围从1到10,每个线程默认优先级为5,你可以使用setPriority()方法设置线程优先级。
创建一个线程的第二种方法是实现Runnable接口。
实现run()方法。然后,创建一个新的Thread对象,将Runnable类传递给它的构造函数,并通过调用start()方法来启动Thread。
下面是一个例子:
class Loader implements Runnable {
public void run() {
System.out.println("Hello Lu");
}
}
class MyJavaClass {
public static void main(String[] args) {
Thread t = new Thread(new Loader());
t.start();
}
}
Thread.sleep()方法将线程暂停指定的时间段。例如,调用Thread.sleep(1000);暂停一秒钟的线程。
请记住,Thread.sleep()抛出一个InterruptedException,所以一定要用try/catch块来包围它。
提示:看起来,实现Runnable接口比从Thread类继承复杂一点。但是,实现Runnable接口是启动线程的首选方法,因为它使你可以从另一个类进行继承。
异常类型
在Java中,有两种异常类型,分别是检查性异常和运行时异常(又叫非检查异常)。
主要区别在于检查性异常在编译时被检查,而在运行时检查未经检查的异常。
检查异常是运行时异常以外的异常, 也是Exception及其子类, 这些异常从程序的角度来说是必须经过捕捉检查处理的, 否则不能通过编译. 如IOException、SQLException等
运行时异常都是RuntimeException类及其子类, 如 NullPointerException、IndexOutOfBoundsException等, 这些异常是不检查的异常, 是在程序运行的时候可能会发生的, 所以程序可以捕捉, 也可以不捕捉. 这些错误一般是由程序的逻辑错误引起的, 程序应该从逻辑角度去尽量避免.
正如在上一个小节中所提到的,Thread.sleep()抛出一个InterruptedException。这是一个检查性异常的例子。在处理异常之前,你的代码将不会编译。如下图所示:
代码如下:
public class MyJavaClass {
public static void main(String[] args) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 其余代码
}
}
}
我们在之前的小节中,有遇到过运行时检查未经检查的异常例子。
下面是一个例子(尝试除以0):
public class MyJavaClass {
public static void main(String[] args) {
int value = 5;
value = value / 0;
}
}
/*
Exception in thread "main" java.lang.ArithmeticException: / by zero
at MyJavaClass.main(MyJavaClass.java:4)
*/
ArrayList
Java API 提供了特殊的类来存储和操作对象组。
有一个这样的类是ArrayList。标准Java数组的长度是固定的,它们在创建之后不能扩展或缩小。
另一方面,ArrayLists是使用初始大小创建的,但是当超过这个大小时,集合会自动增大。
当对象被删除时,ArrayList可能会缩小。请注意,ArrayList类在java.util包中,因此在使用它之前需要导入它。
像任何对象一样创建一个ArrayList。
import java.util.ArrayList;
//...
ArrayList colors = new ArrayList();
你可以选择指定ArrayList将保存的对象的容量和类型:
ArrayList<String> colors = new ArrayList<String>(5);
上面的代码定义了一个初始大小为5的String的ArrayList。
ArrayList类提供了许多有用的方法来处理它的对象。
add()方法将新对象添加到ArrayList。相反,remove()方法从ArrayList中移除对象。
下面是一个例子:
import java.util.ArrayList;
public class MyJavaClass {
public static void main(String[] args) {
ArrayList<String> colors = new ArrayList<String>();
colors.add("Red");
colors.add("Blue");
colors.add("Green");
colors.add("Orange");
colors.remove("Blue");
System.out.println(colors);
}
}
// 输出: [Red,Green,Orange]
ArrayList类其他有用的方法包括:
contains():如果列表包含指定的元素,则返回true, 否则返回false。
get(int index):返回列表中指定位置的元素。
size():返回列表中元素的数量。
clear():删除列表中所有元素。
提示:ArrayList类与数组一样,索引从0开始。
LinkedList
LinkedList在语法上与ArrayList非常相似。
你可以通过更改对象类型,将ArrayList更改为LinkedList。
import java.util.LinkedList;
public class MyJavaClass {
public static void main(String[] args) {
LinkedList<String> c = new LinkedList<String>();
c.add("Red");
c.add("Blue");
c.add("Green");
c.add("Orange");
c.remove("Blue");
System.out.println(c);
}
}
// 输出 [Red,Green,Orange]
提示:不能为LinkedList指定初始容量。
LinkedList 与 ArrayList 的区别
LinkedList和ArrayList最明显的区别在于它们存储对象的方式。
ArrayList更适合于存储和访问数据,因为它与普通数组非常相似。
LinkedList更适合操作数据,比如进行大量的插入和删除操作。
除了存储对象之外,LinkedList还存储相邻元素内存地址(或链接)。
它被称为LinkedList,是因为每个元素都包含到相邻元素的链接。
你可以使用增强型的for循环遍历其元素。
LinkedList<String> c = new LinkedList<String>();
c.add("Red");
c.add("Blue");
c.add("Green");
c.add("Orange");
c.remove("Blue");
for(String s:c) {
System.out.println(s);
}
/* 输出:
Red
Green
Orange
*/
提示:当你需要快速访问数据时,请使用ArrayList;当需要进行大量的插入和删除操作时,请使用LinkedList。
HashMap
数组和列表将元素存储为有序集合,每个元素都有一个整数索引。
HashMap用于将数据集合存储为键和值对。一个对象被用作另一个对象(值)的键(索引)。
put,remove和get方法用于添加,删除和访问HashMap中的值。
下面是一个例子:
import java.util.HashMap;
public class MyJavaClass {
public static void main(String[] args) {
HashMap<String, Integer> points = new HashMap<String, Integer>();
points.put("Shuter", 90);
points.put("Loen", 85);
points.put("Lu", 80);
System.out.println(points.get("Loen"));
}
}
// 输出 85
我们创建了一个HashMap,其中的字符串作为键,整数作为其值。
提示:使用get方法和相应的键来访问HashMap元素。
一个HashMap不能包含重复的键。使用已存在的键添加新项目会覆盖旧元素。
HashMap类提供了containsKey和containsValue方法,用于确定是否存在指定的键或值。
如果尝试获取映射中不存在的值,则返回null值。
import java.util.HashMap;
public class MyJavaClass {
public static void main(String[] args) {
HashMap<String, Integer> points = new HashMap<String, Integer>();
points.put("Shuter", 90);
System.out.println(points.containsKey("Shuter"));
}
}
运行结果:
true
提示:null是表示缺少值的特殊类型。
Set (集合)
**Set是不能包含重复元素的集合。**它模拟数学集抽象。
Set的一个实现是HashSet类。
下面是一个例子:
import java.util.HashSet;
public class MyJavaClass {
public static void main(String[] args) {
HashSet<String> set = new HashSet<String>();
set.add("A");
set.add("B");
set.add("C");
set.add("A");
System.out.println(set);
}
}
// 输出: [A, B, C]
提示:你可以使用size()方法获取HashSet中元素的数量,例如set.size()。
LinkedHashSet
HashSet类不会自动保留添加元素的顺序。若要对元素进行排序,请使用LinkedHashSet,它按照它们插入的顺序来维护集合元素的链接列表。
什么是哈希?
哈希表通过称为哈希的机制存储信息,其中使用键的信息内容来确定称为哈希码的唯一值。
所以,HashSet中的每个元素都与其唯一的哈希码相关联。
提示:Java中可用的集合类型,包括Set(集)、List(列表)和Map(映射),选择使用哪一个取决于你需要存储和操作的数据。
排序列表
为了处理不同集合类型的数据,Java API提供了一个Collections类,它包含在java.util包中。
最常用的Collections类方法之一是sort(),它对集合类型的元素进行排序。Collections类中的方法是静态的,所以不需要一个Collections对象来调用它们。
下面是一个例子:
import java.util.ArrayList;
import java.util.Collections;
public class MyJavaClass {
public static void main(String[] args) {
ArrayList<String> animals = new ArrayList<String>();
animals.add("dog");
animals.add("cat");
animals.add("monkey");
animals.add("bear");
Collections.sort(animals);
System.out.println(animals);
}
}
/* 输出:
[bear,cat,dog,monkey]
*/
如你所见,这些元素的输出结果已经按字母顺序排序了。
你可以在不同类型的列表上调用sort()方法,如Integer。
import java.util.ArrayList;
import java.util.Collections;
public class MyJavaClass {
public static void main(String[] args) {
ArrayList<Integer> nums = new ArrayList<Integer>();
nums.add(12);
nums.add(17);
nums.add(30);
nums.add(33);
nums.add(7);
Collections.sort(nums);
System.out.println(nums);
}
}
/* 输出:
[7,12,17,30,33]
*/
Collections类中其他有用的方法:
max(Collection c):返回由自然顺序决定的c中的最大元素。
min(Collection c):返回由自然顺序决定的c中的最小元素。
reverse(List list): 反转列表中的序列。
shuffle(List list): 将列表中的元素随机排序。
迭代器(Iterators)
迭代器是一个能够循环访问,获取或删除元素的对象。
在通过迭代器访问集合之前,你必须先获得一个集合。**每个集合类都提供了一个iterator()方法,它将一个迭代器返回到集合的开始处。**通过使用这个迭代器对象,你可以访问集合中的每个元素,一次一个元素。
Iterator类提供了以下方法:
hasNext():检查序列中是否还有元素。如果还有元素,则返回true;否则,它返回false。
next():获得序列中的下一个元素。
remove():将迭代器新返回的元素删除。
Iterator类必须从java.util包中导入。
下面是一个例子:
import java.util.Iterator;
import java.util.LinkedList;
public class MyJavaClass {
public static void main(String[] args) {
LinkedList<String> animals = new LinkedList<String>();
animals.add("dog");
animals.add("cat");
animals.add("monkey");
animals.add("bear");
Iterator<String> it = animals.iterator();
String value = it.next();
System.out.println(value);
}
}
//输出 "dog"
提示:it.next() 返回列表中的第一个元素,然后将迭代器移动到下一个元素。每次调用it.next() 时,迭代器都会移到列表的下一个元素。
通常,迭代器被用在循环中。在循环的每次迭代中,都可以访问相应的列表元素。
下面是一个例子:
import java.util.Iterator;
import java.util.LinkedList;
public class MyJavaClass {
public static void main(String[] args) {
LinkedList<String> animals = new LinkedList<String>();
animals.add("dog");
animals.add("cat");
animals.add("monkey");
animals.add("bear");
Iterator<String> it = animals.iterator();
while(it.hasNext()) {
String value = it.next();
System.out.println(value);
}
}
}
/* 输出
dog
cat
monkey
bear
*/
在这里,while循环决定迭代器是否有附加元素,打印元素的值,并将迭代器推进到下一个元素。
使用文件
java.io 包中包含一个File类,允许你使用文件。
首先,创建一个File对象,并在构造函数中指定文件的路径。
import java.io.File;
...
File file = new File("D:\\data\\input-file.txt");
使用exists()方法,你可以确定文件是否存在。
import java.io.File;
public class MyJavaClass {
public static void main(String[] args) {
File x = new File("D:\\edong\\w3cschool.txt");
if(x.exists()) {
System.out.println(x.getName() + "exists!");
}
else {
System.out.println("The file does not exist");
}
}
}
上面的代码打印一条消息,指出文件是否存在于指定的路径。
提示:**getName()方法返回文件的名称。**请注意,我们在路径中使用双反斜杠,因为一个反斜杠应该在字符串路径中转义。
读取文件
文件对于存储和检索数据非常有用,并且有许多方法可以从文件中读取。
最简单的方法之一是使用java.util包中的Scanner类。
Scanner类的构造函数可以将File对象作为输入。
要读取路径“D:\edong\w3cschool.txt”中的文本文件的内容,我们需要创建一个具有相应路径的File对象,并将其传递给Scanner对象。
try {
File x = new File("D:\\edong\\w3cschool.txt");
Scanner sc = new Scanner(x);
}
catch (FileNotFoundException e) {
}
在代码中,我们用try/catch块包围了代码,因为该文件可能不存在。
我们可以使用Scanner对象的next()方法来读取文件的内容。
try {
File x = new File("D:\\edong\\w3cschool.txt");
Scanner sc = new Scanner(x);
while(sc.hasNext()) {
System.out.println(sc.next());
}
sc.close();
} catch (FileNotFoundException e) {
System.out.println("Error");
}
文件的内容逐字输出,因为next() 方法分别返回每个单词。
提示:关闭文件时总是一个好习惯。一种方法是使用Scanner的close()方法。
创建文件
Formatter是java.util包中另一个有用的类,用于创建内容并将其写入文件。
下面是一个例子:
import java.util.Formatter;
public class MyJavaClass {
public static void main(String[] args) {
try {
Formatter f = new Formatter("D:\\edong\\w3cschool.txt");
} catch (Exception e) {
System.out.println("Error");
}
}
}
这将在指定的路径中创建一个空文件。如果该文件已经存在,这将覆盖它。
同样,你需要用try/catch块来包围代码,因为操作可能会失败。
写入文件
一旦创建了文件,就可以使用相同的Formatter对象的format()方法向其中写入内容。
下面是一个例子:
import java.util.Formatter;
public class MyJavaClass {
public static void main(String[] args) {
try {
Formatter f = new Formatter("C:\\edong\\w3cschool.txt");
f.format("%s %s %s","1","Loen","Wang \r\n");
f.format("%s %s %s","2","Lu","Lin");
f.close();
} catch (Exception e) {
System.out.println("Error");
}
}
}
format()方法根据其第一个参数来设置参数的格式。
%s表示一个字符串,并在格式之后被get的第一个参数替换。第二个%s被下一个替换,依此类推。所以,格式%s%s%s表示三个用空格分隔的字符串。
注意:\r\n是Windows中的换行符号。
上面的代码创建一个包含以下内容的文件:
1 Loen Wang
2 Lu Lin
提示:完成写入文件后,不要忘记关闭文件。