待更…
输入
3 2.345678 1 2 3输出
sum=6 6 2.346 1.23 1.234568 Hello world Hello java输入
3 1 2 3输出
5 5007 0 1 2 3 0 Hello sum = 6输入
3 1 2 3输出
10007 5007 Hello sum = 6输入
100输出
Yes No what?import
//package Main; import java.util.*; import java.io.*; import java.lang.*; public class Main{ public static void main(String[] args){ Date date = new Date(); System.out.println(date.toString()); } }java支持类里面定义一个类(内部类)
如果定义为private,那么我们就不能通过new一个类直接调用私密的变量,但是我们可以在类里写一个函数,例如setName,这样我们可以在函数里面防止输入的数据有误,会直接报错。我们也可以调用设置private变量。
public class Main { public static void main(String[] args) { Person ming = new Person(); ming.setName("Xiao Ming"); // 设置name ming.setAge(12); // 设置age System.out.println(ming.getName() + ", " + ming.getAge()); } } 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) { if (age < 0 || age > 100) { throw new IllegalArgumentException("invalid age value"); } this.age = age; } }private方法只能由内部方法调用。
可变参数用类型…定义,可变参数相当于数组类型:
class Group { private String[] names; public void setNames(String... names) { this.names = names; } }完全可以把可变参数改写为String[]类型:
class Group { private String[] names; public void setNames(String[] names) { this.names = names; } }但是,调用方需要自己先构造String[],比较麻烦。例如:
Group g = new Group(); g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]另一个问题是,调用方可以传入null:
Group g = new Group(); g.setNames(null);而可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null。
如果我们传入的是一个数组,那么类似C语言,我们实际上传递的是一个指针(java里没有指针hhh)
public class Main { public static void main(String[] args) { Person p = new Person(); String[] fullname = new String[] { "Homer", "Simpson" }; p.setName(fullname); // 传入fullname数组 System.out.println(p.getName()); // "Homer Simpson" fullname[0] = "Bart"; // fullname数组的第一个元素修改为"Bart" System.out.println(p.getName()); // "Homer Simpson"还是"Bart Simpson"? } } class Person { private String[] name; public String getName() { return this.name[0] + " " + this.name[1]; } public void setName(String[] name) { this.name = name; } }输出:
Homer Simpson Bart Simpson结论:引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象嘛)。
多构造方法 可以定义多个构造方法,在通过new操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分:
class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public Person(String name) { this.name = name; this.age = 12; } public Person() { } }在一个类中,我们可以定义多个方法。如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。例如,在Hello类中,定义多个hello()方法:
class Hello { public void hello() { System.out.println("Hello, world!"); } public void hello(String name) { System.out.println("Hello, " + name + "!"); } public void hello(String name, int age) { if (age < 18) { System.out.println("Hi, " + name + "!"); } else { System.out.println("Hello, " + name + "!"); } } }这种方法名相同,但各自的参数不同,称为方法重载(Overload)。
Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object特殊,它没有父类。
java中只支持单继承,不支持多继承。(其他语言有的支持多继承,例如C++,格式extends 父类1,父类2,…)支持多层继承(形成继承体系)。子类只能继承父类中的非私有成员,但可以通过父类的成员方法调用父类的私有成员变量。子类不能继承父类的构造方法,但可以通过super关键字访问父类的构造方法。当子类和父类不在同一个包的时候,父类里的private和友好访问权限是不会被继承的。(方法同样也是)
方法重写:参数不变,直接重写函数即可。(会隐藏继承的方法) 参数不同是新写了一个方法不是重写(重载)
注意:子类自动获得了父类的所有字段,严禁定义与父类重名的字段!
//package Main; import java.util.*; import java.io.*; import java.lang.*; public class Main{ public static void main(String[] args){ Student stu1 = new Student(); stu1.showpeople(); } } class people { int age = 10, leg = 2, hand = 2; protected void showpeople(){ System.out.printf("%d岁,%d只脚,%d只手\n", age, leg, hand); } } class Student extends people { int number; int add(int x, int y){ return x + y; } }继承有个特点,就是子类无法访问父类的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修饰的字段可以被子类访问
instanceof主要用来判断一个类是否实现了某个接口,或者判断一个实例对象是否属于一个类。
1.判断一个对象是否属于一个类
boolean result = p instanceof Student;它的返回值是一个布尔型的。
2.对象类型强制转换前的判断
Person p = new Student(); //判断对象p是否为Student类的实例 if(p instanceof Student) { //向下转型 Student s = (Student)p; }如果子类声明的成员变量与父类的相同,那么子类继承的父类的成员变量就会被隐藏。 如果需要调用被隐藏的成员变量或者方法可以使用super关键字。
this
this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
this 的用法在 Java 中大体可以分为3种:
普通的直接引用这种就不用讲了,this 相当于是指向当前对象本身。
形参与成员名字重名,用 this 来区分:实例
class Person { private int age = 10; public Person(){ System.out.println("初始化年龄:"+age); } public int GetAge(int age){ this.age = age; return this.age; } } public class test1 { public static void main(String[] args) { Person Harry = new Person(); System.out.println("Harry's age is "+Harry.GetAge(12)); } }运行结果:
初始化年龄:10 Harry's age is 12可以看到,这里 age 是 GetAge 成员方法的形参,this.age 是 Person 类的成员变量。
引用构造函数这个和 super 放在一起讲,见下面。
super
super 可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。 super 也有三种用法:
普通的直接引用与 this 类似,super 相当于是指向当前对象的父类,这样就可以用 super.xxx 来引用父类的成员。
子类中的成员变量或方法与父类中的成员变量或方法同名实例
class Country { String name; void value() { name = "China"; } } class City extends Country { String name; void value() { name = "Shanghai"; super.value(); //调用父类的方法 System.out.println(name); System.out.println(super.name); } public static void main(String[] args) { City c=new City(); c.value(); } }运行结果:
Shanghai China可以看到,这里既调用了父类的方法,也调用了父类的变量。若不调用父类方法 value(),只调用父类变量 name 的话,则父类 name 值为默认值 null。
引用构造函数super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。 this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。 实例
class Person { public static void prt(String s) { System.out.println(s); } Person() { prt("父类·无参数构造方法: "+"A Person."); }//构造方法(1) Person(String name) { prt("父类·含一个参数的构造方法: "+"A person's name is " + name); }//构造方法(2) } public class Chinese extends Person { Chinese() { super(); // 调用父类构造方法(1) prt("子类·调用父类"无参数构造方法": "+"A chinese coder."); } Chinese(String name) { super(name);// 调用父类具有相同形参的构造方法(2) prt("子类·调用父类"含一个参数的构造方法": "+"his name is " + name); } Chinese(String name, int age) { this(name);// 调用具有相同形参的构造方法(3) prt("子类:调用子类具有相同形参的构造方法:his age is " + age); } public static void main(String[] args) { Chinese cn = new Chinese(); cn = new Chinese("codersai"); cn = new Chinese("codersai", 18); } }运行结果:
父类·无参数构造方法: A Person. 子类·调用父类”无参数构造方法“: A chinese coder. 父类·含一个参数的构造方法: A person's name is codersai 子类·调用父类”含一个参数的构造方法“: his name is codersai 父类·含一个参数的构造方法: A person's name is codersai 子类·调用父类”含一个参数的构造方法“: his name is codersai 子类:调用子类具有相同形参的构造方法:his age is 18从本例可以看到,可以用 super 和 this 分别调用父类的构造方法和本类中其他形式的构造方法。
例子中 Chinese 类第三种构造方法调用的是本类中第二种构造方法,而第二种构造方法是调用父类的,因此也要先调用父类的构造方法,再调用本类中第二种,最后是重写第三种构造方法。
super super关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName。例如:
class Student extends Person { public String hello() { return "Hello, " + super.name; } } super(参数):调用基类中的某一个构造函数(应该为构造函数中的第一条语句)this(参数):调用本类中另一种形成的构造函数(应该为构造函数中的第一条语句)super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参) this:它代表当前对象名(在程序中易产生二义性之处,应使用 this 来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用 this 来指明成员变量名)调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用 super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。 super() 和 this() 类似,区别是,super() 从子类中调用父类的构造方法,this() 在同一类内调用其它方法。super() 和 this() 均需放在构造方法内第一行。尽管可以用this调用一个构造器,但却不能调用两个。this 和 super 不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有 super 语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。this() 和 super() 都指的是对象,所以,均不可以在 static 环境中使用。包括:static 变量,static 方法,static 语句块。从本质上讲,this 是一个指向本对象的指针, 然而 super 是一个 Java 关键字。这是因为在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();,所以,Student类的构造方法实际上是这样:
class Student extends Person { protected int score; public Student(String name, int age, int score) { super(); // 自动调用父类的构造方法 this.score = score; } }但是,Person类并没有无参数的构造方法,因此,编译失败。 解决方法是调用Person类存在的某个构造方法。例如:
class Student extends Person { protected int score; public Student(String name, int age, int score) { super(name, age); // 调用父类的构造方法Person(String, int) this.score = score; } }这样就可以正常编译了! 因此我们得出结论:如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。
这里还顺带引出了另一个问题:即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
继承是面向对象编程的一种强大的代码复用方式;Java只允许单继承,所有类最终的根类是Object;protected允许子类访问父类的字段和方法;子类的构造方法可以通过super()调用父类的构造方法;可以安全地向上转型为更抽象的类型;可以强制向下转型,最好借助instanceof判断;子类和父类的关系是is,has关系不能用继承。final类不能被继承,也就是说不能拥有子类 final方法不允许被子类重写。 final +成员变量 = 常量
在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。
Override和Overload不同的是,如果方法签名如果不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override。
注意:方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。
加上@Override可以让编译器帮助检查是否进行了正确的覆写。
Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。 这个非常重要的特性在面向对象编程中称之为多态。
多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。
多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。
package income; public class come { 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); } @Override public double getTax() { if (income <= 5000) { return 0; } return (income - 5000) * 0.2; } } class StateCouncilSpecialAllowance extends Income { public StateCouncilSpecialAllowance(double income) { super(income); } @Override public double getTax() { return 0; } }因为所有的class最终都继承自Object,而Object定义了几个重要的方法: toString():把instance输出为String; equals():判断两个instance是否逻辑相等; hashCode():计算一个instance的哈希值。
调用super 在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。
final 继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final。用final修饰的方法不能被Override
如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final。用final修饰的类不能被继承
对于一个类的实例字段(变量),同样可以用final修饰。用final修饰的字段在初始化后不能被修改,可以在构造方法中初始化final字段
class Person { public final String name; public Person(String name) { this.name = name; } }这种方法更为常用,因为可以保证实例一旦创建,其final字段就不可修改。
把一个方法声明为abstract,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person类也无法被实例化。编译器会告诉我们,无法编译Person类,因为它包含抽象方法。 必须把Person类本身也声明为abstract,才能正确编译它:
abstract class Person { public abstract void run(); }抽象类 如果一个class定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用abstract修饰。 因为无法执行抽象方法,因此这个类也必须申明为抽象类(abstract class)。 使用abstract修饰的类就是抽象类。我们无法实例化一个抽象类:
Person p = new Person(); // 编译错误因为抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法
尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。
面向抽象编程的本质就是:
上层代码只定义规范(例如:abstract class Person);不需要子类就可以实现业务逻辑(正常编译);具体的业务逻辑由不同的子类实现,调用者并不关心。对于abstract方法只允许声明,不允许实现(因为没有方法体)(毕竟叫抽象,当然不能实实在在的让你实现),并且不允许使用final和abstract同时修饰一个方法或者类,也不允许使用static修饰abstract方法。也就是说,abstract方法只能是实例方法,不能是类方法。
既然abstract类和方法这么特殊,我们就必须对比一下它和普通类与方法之间的区别了:
abstract类中可以有abstract方法abstract类中可以有abstract方法,也可以有非abstract方法
非abstract类中不可以有abstract方法
abstract类不能使用new运算符创建对象但是如果一个非抽象类是抽象类的子类,这时候我们想要创建该对象呢,这时候它就必须要重写父类的抽象方法,并且给出方法体,这也就是说明了为什么不允许使用final和abstract同时修饰一个类或者方法的原因。
重点常考!:final和abstract,private和abstract,static和abstract,这些是不能放在一起的修饰符,因为abstract修饰的方法是必须在其子类中实现(覆盖),才能以多态方式调用,以上修饰符在修饰方法时期子类都覆盖不了这个方法,final是不可以覆盖,private是不能够继承到子类,所以也就不能覆盖,static是可以覆盖的,但是在调用时会调用编译时类型的方法,因为调用的是父类的方法,而父类的方法又是抽象的方法,又不能够调用,所以上的修饰符不能放在一起。
abstract类的子类如果一个非abstract类是abstract类的子类,它必须重写父类的abstract方法,也就是去掉abstract方法的abstract修饰,并给出方法体。
如果一个abstract类是abstract类的子类,它可以重写父类的abstract方法,也可以继承父类的abstract方法。
下面举个例子:
abstract class GirlFriend{ //抽象类,封装了两个行为标准 abstract void speak(); abstract void cooking(); } class ChinaGirlFriend extends GirlFriend{ void speak(){ System.out.println("你好"); } void cooking(){ System.out.println("水煮鱼"); } } class AmercanGirlFriend extends GirlFriend{ void speak(){ System.out.println("hello"); } void cooking(){ System.out.println("roast beef"); } } class boy{ GirlFriend friend; void setGirlFriend(GirlFriend f){ friend=f; } void showGirlFriend(){ friend.speaking(); friend.cooking(); } } public class text{ public static void main(String args[]){ GirlFriend girl =new ChineseGirlFriend(); //这里girl是上转型对象 Boy boy=new boy(); boy.setGirlFriend(girl); boy.showGirlFriend(); girl=new AmericanGirlFriend(); //girl 是上转型对象 boy.setGirlFriend(girl); boy.showGirlFriend(); } }小结
通过abstract定义的方法是抽象方法,它只有定义,没有实现。抽象方法定义了子类必须实现的接口规范;定义了抽象方法的class必须被定义为抽象类,从抽象类继承的子类必须实现抽象方法;如果不实现抽象方法,则该子类仍是一个抽象类;面向抽象编程使得调用者只关心抽象方法的定义,不关心子类的具体实现。接口是抽象的抽象(抽象类是具体的抽象)。
例如制作一款运动手表,接口就是产品需要实现的功能。我这款手表要实现与APP的结合,要实现来电的提醒,要实现闹铃的设置,要实现心率的实时监控,要实现步数的记录… 我不会告诉你任何具体的实现方法,我只会给你一个产品功能的框架,而如果你是我团队的一员,要来制作这款运动手表,那么你就一定要把我定义的内容全部实现。
即“如果你是…, ,就必须…”
这就是接口,在程序中,它就相当于是一个类的行为规范。
如果一个抽象类没有字段,所有方法全部都是抽象方法:
abstract class Person { public abstract void run(); public abstract String getName(); }就可以把该抽象类改写为接口:interface。 在Java中,使用interface可以声明一个接口:
interface Person { void run(); String getName(); }所谓interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。
当一个具体的class去实现一个interface时,需要使用implements关键字。
我们知道,在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface
class Student implements Person, Hello { // 实现了两个interface ... }接口继承 一个interface可以继承自另一个interface。interface继承自interface使用extends,它相当于扩展了接口的方法。例如:
interface Hello { void hello(); } interface Person extends Hello { void run(); String getName(); }default方法
在接口中,可以定义default方法。例如,把Person接口的run()方法改为default方法:
实现类可以不必覆写default方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。 default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。
接口的作用(引索)
有利于代码的规范
有利于代码进行维护
有利于代码的安全和严密
丰富了继承的方式
接口声明
关键字:interface public interface 接口名 {}
接口体
常量(没有变量) ( public static final ) int MAX = 100; 可以省略public static final
抽象方法 (public abstract) void add(); 可以省略public abstract
常量和抽象方法都只有一种访问修饰符:public
接口默认提供 public,static,final,abstract 关键字
接口的实现
关键字:implements
类可以实现一个或多个接口 public class Dog implements Eatable,Sleepable Dog 也可以继承一个具体类 public class Dog extends Animal implements Eatable , Sleepable
类中必须重写接口中的全部方法( 抽象类 可只重写接口中的部分方法)
类中重写的方法,访问修饰符必须是 public
接口中定义的常量,在继承了接口的类中可以直接使用。
是什么? 接口名 接口的对象 = 实现了接口的类的对象
该 接口对象 可以调用 被类实现了的 接口方法
public interface Com{} public class Object implements Com{} Com com = new Object(); //接口的回调不同的类在实现同一个接口时可能具有不同的实现方式,那么接口变量在回调接口方法时就可能具有多种形态。
将接口的类的实例的引用传递给该接口参数,那么该参数就可以回调类实现的接口方法。
1.abstract类和接口都可以有abstract方法。 2.接口中只可以有常量,不能有变量;而abstract类中既可以有常量又可以有变量。 3.abstract类中也可以由非abstract方法,接口不可以。
小结
Java的接口(interface)定义了纯抽象规范,一个类可以实现多个接口;接口也是数据类型,适用于向上转型和向下转型;接口的所有方法都是抽象方法,接口不能定义实例字段;接口可以定义default方法Java规定: 必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception。 不需要捕获的异常,包括Error及其子类,RuntimeException及其子类。
捕获异常使用try...catch语句,把可能发生异常的代码放到try {...}中,然后使用catch捕获对应的Exception及其子类:
public class Main { public static void main(String[] args) { byte[] bs = toGBK("中文"); System.out.println(Arrays.toString(bs)); } static byte[] toGBK(String s) { try { // 用指定编码转换String为byte[]: return s.getBytes("GBK"); } catch (UnsupportedEncodingException e) { // 如果系统不支持GBK编码,会捕获到UnsupportedEncodingException: System.out.println(e); // 打印异常信息 return s.getBytes(); // 尝试使用用默认编码 } } }如果我们不捕获UnsupportedEncodingException,会出现编译失败的问题
错误信息类似:unreported exception UnsupportedEncodingException; must be caught or declared to be thrown,并且准确地指出需要捕获的语句是return s.getBytes("GBK");。意思是说,像UnsupportedEncodingException这样的Checked Exception,必须被捕获。
在toGBK()方法中,因为调用了String.getBytes(String)方法,就必须捕获UnsupportedEncodingException。我们也可以不捕获它,而是在方法定义处用throws表示toGBK()方法可能会抛出UnsupportedEncodingException,就可以让toGBK()方法通过编译器检查,我们在main()方法中捕获异常并处理
public class Main { public static void main(String[] args) { try { byte[] bs = toGBK("中文"); System.out.println(Arrays.toString(bs)); } catch (UnsupportedEncodingException e) { System.out.println(e); } } static byte[] toGBK(String s) throws UnsupportedEncodingException { // 用指定编码转换String为byte[]: return s.getBytes("GBK"); } }小结
Java使用异常来表示错误,并通过try … catch捕获异常;Java的异常是class,并且从Throwable继承;Error是无需捕获的严重错误,Exception是应该捕获的可处理的错误;RuntimeException无需强制捕获,非RuntimeException(Checked - Exception)需强制捕获,或者用throws声明;不推荐捕获了异常但不进行任何处理。可以使用多个catch语句,每个catch分别捕获对应的Exception及其子类。JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个catch后,执行catch代码块,然后不再继续匹配。
存在多个catch的时候,catch的顺序非常重要:子类必须写在前面。例如:
public static void main(String[] args) { try { process1(); process2(); process3(); } catch (IOException e) { System.out.println("IO error"); } catch (UnsupportedEncodingException e) { // 永远捕获不到 System.out.println("Bad encoding"); } }对于上面的代码,UnsupportedEncodingException异常是永远捕获不到的,因为它是IOException的子类。当抛出UnsupportedEncodingException异常时,会被catch (IOException e) { ... }捕获并执行。
因此,正确的写法是把子类放到前面:
public static void main(String[] args) { try { process1(); process2(); process3(); } catch (UnsupportedEncodingException e) { System.out.println("Bad encoding"); } catch (IOException e) { System.out.println("IO error"); } }Java的try ... catch机制还提供了finally语句,finally语句块保证有无错误都会执行。上述代码可以改写如下:
public static void main(String[] args) { try { process1(); process2(); process3(); } catch (UnsupportedEncodingException e) { System.out.println("Bad encoding"); } catch (IOException e) { System.out.println("IO error"); } finally { System.out.println("END"); } }注意finally有几个特点:
finally语句不是必须的,可写可不写;finally总是最后执行。如果没有发生异常,就正常执行try { ... }语句块,然后执行finally。如果发生了异常,就中断执行try { ... }语句块,然后跳转执行匹配的catch语句块,最后执行finally。
可见,finally是用来保证一些代码必须执行的。
捕获多种异常
因为处理IOException和NumberFormatException的代码是相同的,所以我们可以把它两用|合并到一起:
public static void main(String[] args) { try { process1(); process2(); process3(); } catch (IOException | NumberFormatException e) { // IOException或NumberFormatException System.out.println("Bad input"); } catch (Exception e) { System.out.println("Unknown error"); } }小结
使用try ... catch ... finally时:
多个catch语句的匹配顺序非常重要,子类必须放在前面;finally语句保证了有无异常都会执行,它是可选的;一个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 } }通过printStackTrace()可以打印出方法的调用栈,类似:
java.lang.NumberFormatException: null at java.base/java.lang.Integer.parseInt(Integer.java:614) at java.base/java.lang.Integer.parseInt(Integer.java:770) at Main.process2(Main.java:16) at Main.process1(Main.java:12) at Main.main(Main.java:5)printStackTrace()对于调试错误非常有用,上述信息表示:NumberFormatException是在java.lang.Integer.parseInt方法中被抛出的,从下往上看,调用层次依次是:
main()调用process1();process1()调用process2();process2()调用Integer.parseInt(String);Integer.parseInt(String)调用Integer.parseInt(String, int)。当发生错误时,例如,用户输入了非法的字符,我们就可以抛出异常。
如何抛出异常?参考Integer.parseInt()方法,抛出异常分两步:
创建某个Exception的实例;用throw语句抛出。下面是一个例子:
void process2(String s) { if (s==null) { NullPointerException e = new NullPointerException(); throw e; } }捕获到异常并再次抛出时,一定要留住原始异常,否则很难定位第一案发现场!
小结
调用printStackTrace()可以打印异常的传播栈,对于调试非常有用;
捕获异常并再次抛出新的异常时,应该持有原始异常信息;
通常不要在finally中抛出异常。如果在finally中抛出异常,应该原始异常加入到原有异常中。调用方可通过Throwable.getSuppressed()获取所有添加的Suppressed Exception。
反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。
反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。
class是由JVM在执行过程中动态加载的。JVM在第一次读取到一种class类型时,将其加载进内存。
每加载一种class,JVM就为其创建一个Class类型的实例,并关联起来。注意:这里的Class类型是一个名叫Class的class。它长这样:
public final class Class { private Class() {} }以String类为例,当JVM加载String类时,它首先读取String.class文件到内存,然后,为String类创建一个Class实例并关联起来:
Class cls = new Class(String);这个Class实例是JVM内部创建的
由于JVM为每个加载的class创建了对应的Class实例,并在实例中保存了该class的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class实例,我们就可以通过这个Class实例获取到该实例对应的class的所有信息。
这种通过Class实例获取class信息的方法称为反射(Reflection)。
如何获取一个class的Class实例?有三个方法:
方法一:直接通过一个class的静态变量class获取:
Class cls = String.class;方法二:如果我们有一个实例变量,可以通过该实例变量提供的getClass()方法获取:
String s = "Hello"; Class cls = s.getClass();方法三:如果知道一个class的完整类名,可以通过静态方法Class.forName()获取:
Class cls = Class.forName("java.lang.String");因为Class实例在JVM中是唯一的,所以,上述方法获取的Class实例是同一个实例。可以用==比较两个Class实例:
Class cls1 = String.class; String s = "Hello"; Class cls2 = s.getClass(); boolean sameClass = cls1 == cls2; // true小结
JVM为每个加载的class及interface创建了对应的Class实例来保存class及interface的所有信息;
获取一个class对应的Class实例后,就可以获取该class的所有信息;
通过Class实例获取class信息的方法称为反射(Reflection);
JVM总是动态加载class,可以在运行期根据条件来控制加载class。
反射
泛型
集合
多线程是Java最基本的一种并发模型
即使是多核CPU,因为通常任务的数量远远多于CPU的核数,所以任务也是交替执行的。
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。
多进程模式(每个进程只有一个线程) 多线程模式(一个进程有多个线程) 多进程+多线程模式(复杂度最高)
进程 vs 线程 进程和线程是包含关系,但是多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。 具体采用哪种方式,要考虑到进程和线程的特点。 和多线程相比,多进程的缺点在于: 创建进程比创建线程开销大,尤其是在Windows系统上; 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。 而多进程的优点在于: 多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。
Java多线程编程的特点又在于: 多线程模型是Java程序最基本的并发模型; 后续读写网络、数据库、Web开发等都依赖Java多线程模型。
线程共包括以下5种状态。
新建状态(New) : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种: (01) 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。 (02) 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。 (03) 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。死亡状态(Dead) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。 Thread和Runnable简介Runnable 是一个接口,该接口中只包含了一个run()方法。它的定义如下:
public interface Runnable { public abstract void run(); }Runnable的作用,实现多线程。我们可以定义一个类A实现Runnable接口;然后,通过new Thread(new A())等方式新建线程。
Thread 是一个类。Thread本身就实现了Runnable接口。它的声明如下:
public class Thread implements Runnable {}Thread的作用,实现多线程。
Thread和Runnable的异同点Thread 和 Runnable 的相同点:都是“多线程的实现方式”。 Thread 和 Runnable 的不同点: Thread 是类,而Runnable是接口;Thread本身是实现了Runnable接口的类。我们知道“一个类只能有一个父类,但是却能实现多个接口”,因此Runnable具有更好的扩展性。 此外,Runnable还可以用于“资源的共享”。即,多个线程都是基于某一个Runnable对象建立的,它们会共享Runnable对象上的资源。 通常,建议通过“Runnable”实现多线程!
输出:
Thread-0 卖票:ticket10 Thread-0 卖票:ticket9 Thread-0 卖票:ticket8 Thread-0 卖票:ticket7 Thread-0 卖票:ticket6 Thread-0 卖票:ticket5 Thread-0 卖票:ticket4 Thread-0 卖票:ticket3 Thread-0 卖票:ticket2 Thread-0 卖票:ticket1 Thread-1 卖票:ticket10 Thread-1 卖票:ticket9 Thread-1 卖票:ticket8 Thread-1 卖票:ticket7 Thread-1 卖票:ticket6 Thread-1 卖票:ticket5 Thread-1 卖票:ticket4 Thread-1 卖票:ticket3 Thread-1 卖票:ticket2 Thread-1 卖票:ticket1 Thread-2 卖票:ticket10 Thread-2 卖票:ticket9 Thread-2 卖票:ticket8 Thread-2 卖票:ticket7 Thread-2 卖票:ticket6 Thread-2 卖票:ticket5 Thread-2 卖票:ticket4 Thread-2 卖票:ticket3 Thread-2 卖票:ticket2 Thread-2 卖票:ticket1在AcWing 的在线编译器里每次的输出都不一样…可能是随机的…
结果说明: (01) ThreadSleep继承于Thread,它是自定义个线程。每个ThreadSleep都会卖出10张票。 (02) 主线程main创建并启动3个ThreadSleep子线程。每个子线程都各自卖出了10张票。
通过Runnable实现一个接口,从而实现多线程。
import java.util.*; public class Main { public static void main(String[] args) { MyThread mt = new MyThread(); Thread ts1 = new Thread(mt); Thread ts2 = new Thread(mt); Thread ts3 = new Thread(mt); ts1.start(); ts2.start(); ts3.start(); } } class MyThread implements Runnable { private int ticket = 10; public void run() { for(int i = 0; i < 20; ++ i) { if(this.ticket > 0) { System.out.println(Thread.currentThread().getName() + "买票:ticket" + this.ticket -- ); } } } }输出:
Thread-0买票:ticket10 Thread-0买票:ticket7 Thread-0买票:ticket6 Thread-2买票:ticket8 Thread-2买票:ticket4 Thread-2买票:ticket3 Thread-2买票:ticket2 Thread-2买票:ticket1 Thread-1买票:ticket9 Thread-0买票:ticket5start() : 它的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。 run() : run()就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程!
class MyThread extends Thread{ public MyThread(String name) { super(name); } public void run(){ System.out.println(Thread.currentThread().getName()+" is running"); } }; public class Demo { public static void main(String[] args) { Thread mythread=new MyThread("mythread"); System.out.println(Thread.currentThread().getName()+" call mythread.run()"); mythread.run(); System.out.println(Thread.currentThread().getName()+" call mythread.start()"); mythread.start(); } }运行结果:
main call mythread.run() main is running main call mythread.start() mythread is running结果说明: (1) Thread.currentThread().getName()是用于获取“当前线程”的名字。当前线程是指正在cpu中调度执行的线程。 (2) mythread.run()是在“主线程main”中调用的,该run()方法直接运行在“主线程main”上。 (3) mythread.start()会启动“线程mythread”,“线程mythread”启动之后,会调用run()方法;此时的run()方法是运行在“线程mythread”上。
Java语言内置了多线程支持。当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行main()方法。在main()方法中,我们又可以启动其他线程。 要创建一个新线程非常容易,我们需要实例化一个Thread实例,然后调用它的start()方法
方法一:从Thread派生一个自定义类,然后覆写run()方法
public class Main { public static void main(String[] args) { Thread t = new MyThread(); t.start(); // 启动新线程 } } class MyThread extends Thread { @Override public void run() { System.out.println("start new thread!"); } }方法二:创建Thread实例时,传入一个Runnable实例:
public class Main { public static void main(String[] args) { Thread t = new Thread(new MyRunnable()); t.start(); // 启动新线程 } } class MyRunnable implements Runnable { @Override public void run() { System.out.println("start new thread!"); } }直接调用Thread实例的run()方法是无效的
可以对线程设定优先级,设定优先级的方法是:
Thread.setPriority(int n) // 1~10, 默认值5优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。
小结
Java用Thread对象表示一个线程,通过调用start()启动一个新线程;
一个线程对象只能调用一次start()方法;
线程的执行代码写在run()方法中;
线程调度由操作系统决定,程序本身无法决定调度顺序;
Thread.sleep()可以把当前线程暂停一段时间。
在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:
New:新创建的线程,尚未执行;Runnable:运行中的线程,正在执行run()方法的Java代码;Blocked:运行中的线程,因为某些操作被阻塞而挂起;Waiting:运行中的线程,因为某些操作在等待中;Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;Terminated:线程已终止,因为run()方法执行完毕。当线程启动后,它可以在Runnable、Blocked、Waiting和Timed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。
线程终止的原因有:
线程正常终止:run()方法执行到return语句返回;线程意外终止:run()方法因为未捕获的异常导致线程终止;对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行
如果t线程已经结束,对实例t调用join()会立刻返回。此外,join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。
小结
Java线程对象Thread的状态包括:New、Runnable、Blocked、Waiting、Timed Waiting和Terminated;
通过对另一个线程对象调用join()方法可以等待其执行结束;
可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;
对已经运行结束的线程调用join()方法会立刻返回。
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。
中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。
public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(1); // 暂停1毫秒 t.interrupt(); // 中断t线程 t.join(); // 等待t线程结束 System.out.println("end"); } } class MyThread extends Thread { public void run() { int n = 0; while (! isInterrupted()) { n ++; System.out.println(n + " hello!"); } } }另一个常用的中断线程的方法是设置标志位。我们通常会用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束
public class Main { public static void main(String[] args) throws InterruptedException { HelloThread t = new HelloThread(); t.start(); Thread.sleep(1); t.running = false; // 标志位置为false } } class HelloThread extends Thread { public volatile boolean running = true; public void run() { int n = 0; while (running) { n ++; System.out.println(n + " hello!"); } System.out.println("end!"); } }注意:共享的变量用关键字volatile声明!
因此,volatile关键字的目的是告诉虚拟机: 每次访问变量时,总是获取主内存的最新值; 每次修改变量后,立刻回写到主内存。 volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
小结
对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException;
目标线程检测到isInterrupted()为true或者捕获了InterruptedException都应该立刻结束自身线程;
通过标志位判断需要正确使用volatile关键字;
volatile关键字解决了共享变量在线程间的可见性问题。
如何创建守护线程呢?方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:
Thread t = new MyThread(); t.setDaemon(true); t.start();在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
小结 守护线程是为其他线程服务的线程; 所有非守护线程都执行完毕后,虚拟机退出; 守护线程不能持有需要关闭的资源(如打开文件等)。
Java程序使用synchronized关键字对一个对象进行加锁:
synchronized(lock) { n = n + 1; }synchronized保证了代码块在任意时刻最多只有一个线程能执行。
我们来概括一下如何使用synchronized:
找出修改共享变量的线程代码块;选择一个共享实例作为锁;使用synchronized(lockObject) { ... }。在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁:
public void add(int m) { synchronized (obj) { if (m < 0) { throw new RuntimeException(); } this.value += m; } // 无论有无异常,都会在此释放锁 }JVM规范定义了几种原子操作:
基本类型(long和double除外)赋值,例如:int n = m;引用类型赋值,例如:List<String> list = anotherList。小结 多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步; 同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码; 注意加锁对象必须是同一个实例; 对JVM定义的单个原子操作不需要同步。
当我们锁住的是this实例时,实际上可以用synchronized修饰这个方法。下面两种写法是等价的:
public void add(int n) { synchronized(this) { // 锁住this count += n; } // 解锁 } public synchronized void add(int n) { // 锁住this count += n; } // 解锁因此,用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。
小结 用synchronized修饰方法可以把整个方法变为同步代码块,synchronized方法加锁对象是this; 通过合理的设计和数据封装可以让一个类变为“线程安全”; 一个类没有特殊说明,默认不是thread-safe;