Java基础
1.interface
引用数据类型:类、数组、接口
1.1 概述
接口是除了类和数组之外,另外一种引用数据类型
接口和类不同,类的内部封装了成员变量、构造方法和成员方法,而接口的内部主要就是封装了方法和静态常量。
接口的定义和类很类似,但是接口需要使用 interface
关键字来定义
接口最终也会被编译成.class文件,但一定要明确接口并不是类,而是另外一种引用数据类型
例如,
//使用interface关键字来定义接口
public interface Action {
//接口中的静态常量
public static final String OPS_MODE = "auto";
//接口中的抽象方法
public void start();
//接口的抽象方法
public void stop();
}
注意1, 定义类使用关键字class
,定义接口使用关键字interface
注意2,接口中的属性都是公共的静态常量
注意:常量的名字一般需要全大写
注意3,接口中的方法都是抽象方法
注意:JDK8中,还允许在接口中编写静态方法和默认方法
注意:JDK9中,还允许在接口中编写私有方法
注意4,接口的中抽象方法,不需要使用abstract
修饰符,因为接口中的方法默认就是抽象方法
在接口中,可把一些修饰符省去不写:
//使用interface关键字来定义接口
public interface Action {
//接口中的静态常量
String OPS_MODE = "auto";
//接口中的抽象方法
void start();
//接口的抽象方法
void stop();
}
接口里面的属性,默认就是public static final修饰的
接口里面的方法,默认就是public abstract修饰的
所以这些都可以省去不写的
含有静态方法和默认方法的接口:(JDK8)
public interface Action {
public default void run1() {
// 执行语句
}
public static void run2() {
// 执行语句
}
}
含有私有方法的接口:(JDK9)
public interface Action {
private void run() {
// 执行语句
}
}
注意,我们一般所使用的接口,大多数都只是含义抽象方法
1.2 接口实现
类和类之间的关系是继承,类和接口之间的关系是实现:一个类实现了一个或多个接口
接口和类不同,类可以实例化创建对象,而接口不能创建对象,只能让其他类来实现。
一个类实现了一个接口,那么该类可以称为这个接口的实现类。类实现一个接口 和 类继承一个父类 的效果类似,格式相仿,只是关键字不同,实现使用 implements 关键字,而继承使用的是extends关键字。
一个类实现了接口,那么就要实现接口中所有的抽象方法,否则这个类自己就必须声明为抽象类。
例如,实现一个接口
public interface Action {
void run();
void sayHello();
}
//类实现接口,没有实现接口中的抽象方法,那么这个类就必须声明为抽象类
abstract class Student implements Action{
}
public interface Action {
void run();
void sayHello();
}
//类实现接口,并且实现了接口中所有的抽象方法
class Student implements Action{
public void run(){
//...
}
public void sayHello(){
//...
}
}
例如,实现多个接口
public interface Action {
void run();
void sayHello();
}
public interface Mark{
void star();
}
//类实现接口,并且实现了接口中所有的抽象方法
class Student implements Action, Mark{
public void run(){
//...
}
public void sayHello(){
//...
}
public void star(){
//...
}
}
一个类实现了多个接口,那么就需要把这多个接口中的抽象想法全都实现
JavaAPI中的String
类,也实现了多个接口:
可以看出,String类继承Object类的同时,又实现了三个接口
1.3 接口继承
java中,类和类之间是单继承,接口和接口之间是多继承
例如,
//实现该接口的类,将具有run的功能
public interface Runable {
void run();
}
//实现该接口的类,将具有fly的功能
interface Flyable{
void fly();
}
//实现该接口的类,将具有run的功能,fly的功能,以及Action接口独有的doSomething功能
interface Action extends Runable,Flyable{
void doSomething();
}
//实现类,实现Action接口,就必须要实现Action及其父接口中的所有抽象方法
class Demo implements Action{
@Override
public void run() {
}
@Override
public void fly() {
}
@Override
public void doSomething() {
}
}
1.4 多态
多态的前提是继承,必须要先有子父类关系才行,而类和接口之间的实现关系,其实也是继承的一种形式,所以在类和接口的实现关系中,也可以使用多态
接口的引用执行它的实现类对象:
public interface Action {
void run();
}
class Student implements Action{
@Override
public void run() {
}
}
class Teacher implements Action{
@Override
public void run() {
}
}
class StudentTest{
public static void main(String[] args) {
//声明接口的引用
Action a;
//可以指向它任意一个实现类对象
a = new Student();
a = new Teacher();
}
}
接口的引用调用方法,调用到的是实现类中重写的方法
class StudentTest{
public static void main(String[] args) {
Action a = new Student();;
//调用是到的方法是Student中重写(实现)的方法
a.run();
}
}
注意,抽象方法的实现,也算是一种方法的重写,形式、语法完全一致。
1.5 案例
public interface Action {
void run();
}
interface Mark{
void star();
}
class Student implements Action,Mark{
@Override
public void run() {
System.out.println("student run...");
}
@Override
public void star() {
System.out.println("student star...");
}
}
思考,下面代码是否正确?:
class StudentTest{
public static void main(String[] args) {
Action a = new Student();
a.run();
a.star();
}
}
mian方法中,a.run()
编译运行都是正确的,但是 a.star()
方法的调用是编译错误的。
因为,在编译的时候,编译器会先检查引用a
所属的类型(Action
)中,是否存在当前要调用的方法(star
),如果没有那么就直接编译报错。
那么在这种情况下,该如何修改代码,才能正确调用到star
方法?
class StudentTest{
public static void main(String[] args) {
Action a = new Student();
a.run();
Mark m = (Mark)a;
m.star();
}
}
在这里,可以先做类型强制转换,把Action类型的引用a,转账转为Mark类型的引用m,然后使用引用m,就可以调用到Mark接口中的star方法,同时Student中对star进行了实现(重写),那么最后调用的是Student中的star方法。
思考,上面代码中,Action类型的引用a,为什么可以转换为Mark类型的引用m?注意,Action和Mark之间并没有任何关系
Action a = new Student();
Mark m = (Mark)a;
引用a,能否转账转为Mark类型,主要是看引用a,所指向的对象,是否实现了接口Mark,如果是的话,那么就可以转换成功。
同时,也可以是instanceof关键字,对引用a所指向的对象进行判断,是否属于Mark类型,如果是就说明该对象实现了Mark接口,属于Mark类型的,例如
Action a = new Student();
//返回true,则表示a指向的对象,也同时实现了Mark接口,属于Mark类型,那么就可以做强制转换
if(a instanceof Mark){
Mark m = (Mark)a;
}
思考,如何让开发人员,一定能按照我们的预先设计好的方法,去编写代码?
把所有要实现的方法都放在接口里面,再让开发人员去实现接口
==思考,如果想要实现一个接口,但是不想重写里面的个别方法?
定义一个中间类,空实现接口中所有的类,然后再写个类去继承中间类,然后重写你想要的方法。
2 访问控制
对象中的属性和方法,是可以根据指定修饰符来进行访问控制的。具体控制的就是,这些属性和方法可以在什么地方被访问,以及在什么地方不能被访问。
2.1 概述
类中的属性和方法,可以使用以下四种修饰符进行访问控制:
public > protected > default > private
-
public,公共的,在所有地方都可以访问
-
protected,受保护的,当前类中、子类中,同一个包中其他类中可以访问
-
default,默认的,当前类中、同一个包中的子类中可以访问
注意,default默认的,指的是空修饰符,并不是default这个关键字例如,String name; 在类中,这种情况就是默认的修饰符
-
private,私有的,当前类中可以访问
修饰符 | 类中 | 同包非子类 | 同包子类 | 不同包子类 | 不同包非子类 |
---|---|---|---|---|---|
public | Y | Y | Y | Y | Y |
protected | Y | Y | Y | Y | N |
default | Y | Y | Y | N | N |
private | Y | N | N | N | N |
2.2 案例
例如,
package com.briup.day09.demo;
public class AccessControl {
public String public_str = "public_str";
protected String protected_str = "protected_str";
String default_str = "default_str";
private String private_str = "private_str";
//在当前类中访问
public void test(){
System.out.println(public_str);
System.out.println(protected_str);
System.out.println(default_str);
System.out.println(private_str);
}
}
class SamePackage{
//在同包的非子类中访问
public void test(){
//需要先创建对象,然后再访问
AccessControl ac = new AccessControl();
System.out.println(ac.public_str);
System.out.println(ac.protected_str);
System.out.println(ac.default_str);
//编译报错,无法访问
//System.out.println(ac.private_str);
}
}
class SamePackageSubClass extends AccessControl{
//在同包的子类中访问
public void test(){
//继承后,可直接访问
System.out.println(public_str);
System.out.println(protected_str);
System.out.println(default_str);
//编译报错,无法访问
//System.out.println(ac.private_str);
}
}
在另一个包中,创建类进行访问测试:
package com.briup.day09.other;
import com.briup.day09.demo.AccessControl;
public class OtherPackage {
//在不同包的非子类中访问
public void test(){
//需要先创建对象,然后再访问
AccessControl ac = new AccessControl();
System.out.println(ac.public_str);
//编译报错,无法访问
//System.out.println(ac.protected_str);
//System.out.println(ac.default_str);
//System.out.println(ac.private_str);
}
}
class OtherPackageSubClass extends AccessControl{
//在不同包的子类中访问
public void test(){
//继承后,可直接访问
System.out.println(public_str);
System.out.println(protected_str);
//编译报错,无法访问
//System.out.println(default_str);
//System.out.println(ac.private_str);
}
}
思考,正常情况下,编写一个类,都可以使用哪些权限控制修饰符?
正常编写的类,可以使用俩种权限控制修饰符:public和default
例如,
public class Person{}
class Student extends Person{}
但是,如果是内部类的话,则可以使用四种权限控制修饰符:
例如,
public class Test{
private class A{}
class B{}
protected class C{}
public class D{}
}
在Test类的内部,嵌套了四个内部类:A B C D,分别使用了private、default、protected、public进行了修饰
3 内部类
内部类,不是在一个java源文件中编写俩个平行的类,而是在一个类的内部再定义另外的一个类。
例如,这种情况是不是内部类,而是俩个独立的类
public class Test{
}
class A{
}
例如,这种情况是,是内部类,一个类内部嵌套了另一个类
//Test是外部类
public class Test{
//A是Test类中的内部类
public class A{
}
}
内部类一共分为四种形式:
- 成员内部类
- 静态内部类
- 局部内部类
- 匿名内部类
注意,在A类中,嵌套声明了一个类B,以下都会将A类称为外部类,将B类称为内部类
class 外部类 {
class 内部类{
}
}
3.1 成员内部类
在类中,可以定义成员方法、成员变量,除此之外,还可以定义成员内部类
例如,
//外部类
public class MemberOuterClass{
//外部类的属性
private String name;
private static int age;
//外部类的方法
public void run(){}
public static void go(){}
/* 成员内部类 声明开始 */
public class MemberInnerClass{
private String name;
private int age;
public void run(String name){}
}
/* 成员内部类 声明结束 */
}
注意,成员内部类中,【不能】编写静态的属性和方法
注意,当前这个代码,编译成功后,会生成俩个class文件,一个对应外部类,一个对应内部类
编译生成的俩个class文件的名字分别为:
MemberOuterClass.class
MemberOuterClass$MemberInnerClass.class
成员内部类和外部类的相互访问:
- 成员内部类访问外部的属性和方法
public class MemberOuterClass {
//外部类的属性
private String name;
private static int age;
//外部类的方法
public void run(){}
public static void go(){}
/* 成员内部类 声明开始 */
public class MemberInnerClass{
private String name;
private int age;
public void run(String name){
//访问当前run方法中的参数name
System.out.println(name);
//访问内部类自己的属性name
System.out.println(this.name);
//访问外部类的非静态属性
System.out.println(MemberOuterClass.this.name);
//访问外部类的静态属性
System.out.println(MemberOuterClass.age);
//访问外部类的非静态方法
MemberOuterClass.this.run();
//访问外部类的静态方法
MemberOuterClass.go();
}
}
/* 成员内部类 声明结束 */
}
- 外部类访问成员内部类的属性和方法
public class MemberOuterClass {
//外部类的方法,访问成员内部类的属性和方法
public void test(){
//需要创建内部类对象,然后才可以访问
MemberInnerClass t = new MemberInnerClass();
System.out.println(t.name);
System.out.println(t.age);
t.run("tom");
}
/* 成员内部类 声明开始 */
public class MemberInnerClass{
private String name;
private int age;
public void run(String name){
}
}
/* 成员内部类 声明结束 */
}
在其他类中使用这个内部类:
如果这个成员内部类不是private修饰的,那么在其他类中就可以访问到这个内部类
package com.briup.day09.demo;
import com.briup.day09.demo.MemberOuterClass.MemberInnerClass;
public class Test {
public static void main(String[] args) {
MemberOuterClass moc = new MemberOuterClass();
MemberInnerClass mic = moc.new MemberInnerClass();
mic.run("tom");
}
}
在其他类中,使用这个非private修饰的成员内部类的时候,需要注意以下几点:
- 这个内部类需要import导入,并且是
外部类.内部类
的形式导入。- 在创建对象的时候,需要先创建出外部类对象,然后使用外部类对象再创建内部类对象。
形式为:外部类对象.new 内部类对象();
JavaAPI中的使用的成员内部类:
java.util.ArrayList类中,就定义了好几个成员内部类,并且还是private修饰的
思考,类的内部除了嵌套另一个类之外,是否还可以嵌套接口?
可以,不仅类中可以嵌套接口,接口的内部也可以嵌套其他接口。
例如,参考java.util.Map接口中的内部接口Entry
思考,什么情况下会使用内部类?
在对事物进行抽象的时候,若一个事物内部还包含其他事物,就可以考虑使用内部类这种结构。
例如,汽车(Car)中包含发动机(Engine) ,这时, Engine 类就可以考虑(非必须)使用内部类来描述,定义在Car类中的成员位置。
这样设计,既可以表示Car和Engine的紧密联系的程度,也可以在Engine类中很方便的使用到Car里面的属性和方法
例如,
public class Car { //外部类
class Engine { //内部类
}
}
例如,人(Person)中包含心脏(Heart),这时,Heart类可以考虑(非必须)使用内部类来描述,定义在Person类中的成员位置。
这样设计,既可以表示Person和Heart的紧密联系的程度,也可以在Heart类中很方便的使用到Person里面的属性和方法
例如,
public class Person { //外部类
class Heart { //内部类
}
}
注意,这是从程序中,类和类之间的关系和意义进行考虑而设计的,其实这里即使不使用内部类的结构,使用普通的俩个类也能完成功能,但是内部类的结构设计会加符合实际意义,也能够好的完成功能,因为内部类访问外部的属性和方法会更加容易。
3.2 静态内部类
静态内部类和成员内部类是类似的,只是这个内部类,多了static关键字进行修饰。
例如,
//外部类
public class StaticOuterClass{
/* 静态内部类 声明开始 */
public static class StaticInnerClass{
}
/* 静态内部类 声明结束 */
}
注意,静态内部类中,【可以】编写静态的属性和方法,另外在四种内部类中,只有静态内部类可以编写静态属性和方法
编译生成的俩个class文件的名字分别为:
StaticOuterClass.class
StaticOuterClass$StaticInnerClass.class
静态内部类和外部类的相互访问:
- 静态内部类访问外部的属性和方法
public class StaticOuterClass {
//外部类的属性
private String name;
private static int age;
//外部类的方法
public void run(){}
public static void go(){}
/* 静态内部类 声明开始 */
public static class StaticInnerClass{
private String name;
private static int age;
public void run(String name){
//访问当前run方法中的参数name
System.out.println(name);
//访问内部类自己的属性name
System.out.println(this.name);
//访问内部类自己的静态属性age
System.out.println(age);
//静态内部类中,无法访问外部类的非静态属性和方法
//System.out.println(StaticOuterClass.this.name);
//StaticOuterClass.this.run();
//访问外部类的静态属性和方法
System.out.println(StaticOuterClass.age);
StaticOuterClass.go();
}
}
/* 静态内部类 声明结束 */
}
注意,在静态内部类中访问不了外部类中的非静态属性和方法
- 外部类访问静态内部类的属性和方法
public class StaticOuterClass {
public void test(){
//外部类中,访问静态类中的静态属性
System.out.println(StaticInnerClass.age);
//外部类中,访问静态内部类中的非静态属性和方法
StaticInnerClass sic = new StaticInnerClass();
System.out.println(sic.name);
sic.run("tom");
}
/* 静态内部类 声明开始 */
public static class StaticInnerClass{
private String name;
private static int age;
public void run(String name){
}
}
/* 静态内部类 声明结束 */
}
在其他类中使用这个内部类:
如果这个静态内部类不是private修饰的,那么在其他类中就可以访问到这个内部类
import com.briup.sync.StaticOuterClass.StaticInnerClass;
public class Test {
public static void main(String[] args) {
StaticInnerClass sic = new StaticInnerClass();
sic.run("tom");
}
}
在其他类中,使用这个非private修饰的静态内部类的时候,需要注意以下几点:
- 这个内部类需要import导入,并且是
外部类.内部类
的形式导入。- 在创建对象的时候,直接使用这个静态内部类的名字即可:
new 静态内部类对象();
,不再需要依赖外部类对象了。
JavaAPI中的使用的静态内部类:
java.lang.Integer类中,就定义了一个静态内部类,并且还是private修饰的
思考,观察Integer类中私有静态内部类IntegerCache,它的作用是什么?
Integer num1 = Integer.valueOf(127);
Integer num2 = Integer.valueOf(127);
System.out.println(num1 == num2); //输出结果为 true
Integer num3 = Integer.valueOf(128);
Integer num4 = Integer.valueOf(128);
System.out.println(num3 == num4);//输出结果为 false
注意,==表示要比较这个俩个对象的内存地址是否相等
3.3 局部内部类
局部内部类,是另一种形式的内部,在声明在外部类的方法中,相当于方法中的局部变量的位置,它的作用范围只是在当前方法中。
局部内部类是最不常用的一种内部类。
例如,
public class LocalOuterClass{
public void sayHello(String name){
/* 局部内部类 声明开始 */
class LocalInnerClass{
}
/* 局部内部类 声明结束 */
}
}
局部内部类和外部类的相互访问:
- 局部内部类访问外部的属性和方法
public class LocalOuterClass {
//外部类的属性
private String name;
private static int age;
//外部类的方法
public void run(){}
public static void go(){}
public void sayHello(String name){
/* 局部内部类 声明开始 */
class LocalInnerClass{
private String name;
public void test(String name){
//访问当前test方法中的参数name
System.out.println(name);
//访问内部类自己的属性name
System.out.println(this.name);
/*注意,sayHello方法的参数name,无法访问,因为实在没有办法表示了,换成其他名字后,就可以访问了,不要叫name就行*/
//访问外部类的非静态属性
System.out.println(LocalOuterClass.this.name);
//访问外部类的非静态方法
LocalOuterClass.this.run();
//访问外部类的静态属性和方法
System.out.println(LocalOuterClass.age);
LocalOuterClass.go();
}
}
/* 局部内部类 声明结束 */
}
}
- 局部内部类中,访问当前方法中的变量,这个变量必须是final修饰的
public void sayHello(final String name){
final int num = 1;
/* 局部内部类 声明开始 */
class LocalInnerClass{
public void test(){
System.out.println(name);
System.out.println(num);
//编译报错,final修饰的变量,只能赋值一次
//name = "tom";
//num = 2;
}
}
/* 局部内部类 声明结束 */
}
在JDK1.8中,一个局部变量在局部内部类中进行方法了,那么这个局部变量自动变为final修饰
- 外部类访问局部内部类的属性和方法
public void sayHello(String name){
/* 局部内部类 声明开始 */
class LocalInnerClass{
private int num;
public void test(){
}
}
/* 局部内部类 声明结束 */
//创建局部内部类对象
LocalInnerClass lic = new LocalInnerClass();
//对象访问属性
System.out.println(lic.num);
//对象调用方法
lic.test();
}
局部内部类,只能在当前声明的方法中进行使用。
3.4 匿名内部类
匿名内部类,是一种没有名字的内部类,它是内部类的一种简化写法。在之后的代码中,匿名内部类是使用最多的一种内部类。(很重要)
在普通的代码中,使用一个接口的步骤如下:
- 声明一个类,去实现这个接口
- 实现这个接口中的抽象方法(重写)
- 在其他代码中,创建这个类的对象
- 调用类中实现(重写)后的方法
其实,在这个过程中,我们的目的就是把接口中的抽象方法给实现(重写)了,最后再调用到这个实现后(重写)的方法。
那么,使用匿名内部类,就可以把这个过程给给简化了,让我们更加方法的调用到实现(重写)后的方法!
格式:
父类或者接口类型 变量名 = new 父类或者接口(){
// 方法重写
@Override
public void method() {
// 执行语句
}
};
//调用实现(重写)后的方法
变量名.method();
匿名内部类的俩种形式:
- 利用一个父类,进行声明并创建匿名内部类对象,这个匿名内部类默认就是这个父类的子类型
- 利用一个接口,进行声明并创建匿名内部类对象,这个匿名内部类默认就是这个接口的实现类
匿名内部类因为没有类名:
- 匿名内部类必须依托于一个父类型或者一个接口
- 匿名内部类在声明的同时,就必须创建出对象,否则后面就没法创建了
- 匿名内部类中无法定义构造器
例如,利用父类型来声明并创建匿名内部类对象
public abstract class Animal {
public abstract void run();
}
class Test{
public static void main(String[] args) {
Animal animal = new Animal(){
@Override
public void run() {
System.out.println("匿名内部类中的默认实现");
}
};
animal.run();
}
}
注意,如果利用父类型声明这个匿名内部类,那么这个匿名内部类默认就是这个父类型的子类
例如,利用接口来声明并创建匿名内部类对象
public interface Action {
void run();
}
class Test{
public static void main(String[] args) {
Action a = new Action(){
@Override
public void run() {
System.out.println("匿名内部类中的默认实现");
}
};
a.run();
}
}
注意,如果利用接口声明这个匿名内部类,那么这个匿名内部类默认就是这个接口的实现类
思考,对比之前普通的方式,匿名内部类是不是在调用到重写方法的同时,简化了之前很多步骤?
案例:
//Algorithm 算法接口
public interface Algorithm{
void sort(int[] arr);
}
class Test{
//使用指定算法,对数组arr进行排序
public void sort(int[] arr,Algorithm alg){
alg.sort(arr);
}
}
public static void main(String[] args){
Test t = new Test();
int[] arr = {4,1,6,3,8,5,9};
Algorithm alg = new Algorithm(){
public void sort(int[] arr){
//使用当前需要的排序算法
//例如,这里简单的使用Arrays工具类中的排序方法
java.util.Arrays.sort(arr);
}
};
t.sort(arr,alg);
}
内部类的选择:
假设现在已经确定了要使用内部类,那么一般情况下,该如何选择?
- 考虑这个内部类,如果需要反复的进行多次使用(必须有名字)
- 在这个内部类中,如果需要定义静态的属性和方法,选择使用静态内部类
- 在这个内部类中,如果需要访问外部类的非静态属性和方法,选择使用成员内部类
- 考虑这个内部类,如果只需要使用一次(可以没有名字)
- 选择使用匿名内部类
- 局部内部类,几乎不会使用
4 包装类
java中的八种基本数据类型,它们只能表示一些最简单的数字,这些数字最小的在内存中占8位,最大占64位。这些都是简单的数字,不是对象,所以也不能用来调用方法或者属性。
4.1 概述
针对这八种基本类型,JavaAPI又专门提供了对应的类类型,目的就是为了分别把这八种基本类型的数据,包装成对应的类类型,这时候就变成对象了,就可以调用方法了或者访问属性了。
基本类型 | 包装类型 |
---|---|
boolean | java.lang.Boolean |
byte | java.lang.Byte |
short | java.lang.Short |
int | java.lang.Integer |
long | java.lang.Long |
float | java.lang.Float |
double | java.lang.Double |
char | java.lang.Character |
4.2 案例
在这些包装类型中,都定义相关的属性和方法,例如Integer中:
public static void main(String[] args) {
int i = 1;
//编译报错,因为i不是对象
//i.toString();
//int --> Integer
//Integer o = new Integer(i);
Integer o = Integer.valueOf(1);
//Integer --> int
int j = o.intValue();
System.out.println(o.toString());
System.out.println(Integer.MIN_VALUE);
System.out.println(Integer.MAX_VALUE);
//把100分别转为十进制、二进制、八进制、十六进制的字符串形式
System.out.println(Integer.toString(100));
System.out.println(Integer.toString(100,2));
System.out.println(Integer.toString(100,8));
System.out.println(Integer.toString(100,16));
//把100转为二进制形式
System.out.println(Integer.toBinaryString(100));
//把字符串"100"转为int类型的100
int num = Integer.parseInt("100");
System.out.println(num);
}
4.3 自动装箱/拆箱
JDK1.5或以上,可以支持基本类型和包装类型之间的自动装箱、自动拆箱:
简化了基本类型和包装类型之间的转换,这里以int和Integer为例说明
//JKD1.5 之前
Integer o = new Integer(1);
Integer o = Integer.valueOf(1);
//JDK1.5 之后
//自动装箱,这里会自动把数字1包装成Integer类型的对象
Integer o = 1;
//JKD1.5 之前
Integer o = new Integer(1);
int i = o.intValue();
//JDK1.5 之后
Integer o = new Integer(1);
//自动拆箱,这里会自动把对象o拆箱为一个int类型的数字,并把数字赋值给int类型的变量i
int i = o;
其他的基本类型和包装类型之间的转换,与此类似
注意事项:
public class Test{
public void test1(int i){}
public void test2(Integer i){}
public void test3(long i){}
public void test4(Long i){}
public static void main(String[] args){
t.test1(1);//编译通过 int i = 1; 正常赋值
t.test2(1);//编译通过 Integer i = 1; 自动装箱
t.test3(1);//编译通过 long i = 1; 隐式类型转换
//编译报错
//错误的代码:Long i = 1;
//int和Long 之间没有任何关系
t.test4(1);
t.test4(1L);//编译通过 Long i = 1L; 自动装箱
}
}
5 Object中常用方法
Object类是所有类的父类型,类中定义的方法,java中所有对象都可以调用
5.1 toString方法
该方法可以返回一个对象默认的字符串形式:
public class Object{
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
}
子类中可以对该方法进行重写:
public class Student{
private String name;
private int age;
//get
//set
public String toString() {
return "Student[name="+name+",age="+age+"]";
}
}
toString方法的调用:
public static void main(String[] args){
Student stu = new Student();
//这个俩个输出语句的效果是一样的
System.out.println(stu);
System.out.println(stu.toString());
}
注意,默认清下,println方法会调用这个对象的toString方法
注意,推荐使用第一种,因为当stu的值为null时,第二种输出方式会报错,空指针异常
5.2 getClass方法
该方法是非常重要的一种方法,它可以返回一个引用在运行时所指向的对象,具体类型是什么
该方法是native修饰的本地方法,不是java语言实现的。
public class Object{
public final native Class<?> getClass();
}
子类中不能重写getClass,调用的一定是Object中的getClass方法:
public void test(Object obj){
//obj可以接收任意类型的对象
//getClass方法可以返回obj指向的对象具体是什么类型的
//也就是该对象到底是使用哪个类创建出的对象
System.out.println(obj.getClass());
}
5.3 equals方法
该方法可以比较俩个对象是否相等
public class Object{
public boolean equals(Object obj) {
return (this == obj);
}
}
可以看出,Object中的equals方法,是直接使用的==号进行的比较,比较俩个对象的地址值是否相等
例如,
public static void main(String[] args){
Student o1 = new Student();
Student o2 = new Student();
boolean f = o1.equals(o2);
System.out.println(f);
}
子类中可以对该方法进行重写:
public class Student {
private String name;
private int age;
public Student(){}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
//如果obj为null,直接返回false
if(obj == null){
return false;
}
//如果obj和this的地址值相等,直接返回true
if(this == obj){
return true;
}
//如果obj不属于Student类型的对象,直接返回false
if(!(obj instanceof Student)){
return false;
}
//obj属于Student类型的对象,做类型强制转换
Student other = (Student) obj;
//如果obj的name和age分别等于this的name和age,直接返回true
if(this.name.equals(other.name) && this.age==other.age){
return true;
}
//其他情况,直接返回false
return false;
}
}
class StudentTest{
public static void main(String[] args) {
Student s1 = new Student("tom",20);
Student s2 = new Student("tom",20);
System.out.println(s1 == s2);//输出false,因为俩对象的内存地址不同
System.out.println(s1.equals(s2));//输出true,因为重写了equals的比较规则,name和age相等就算俩对象相等
}
}
按照重写后的equals比较规则,只要俩个对象的name和age属性值都相等,那么就返回true,说明俩对象相等
对equals方法重写,一般需要注意以下几点:
- 自反性:对任意引用obj,obj.equals(obj)的返回值一定为true.
- 对称性:对于任何引用o1、o2,当且仅当o1.equals(o2)返回值为true时,o2.equals(o1)的返回值一定为true;
- 传递性:如果o1.equals(o2)为true, o2.equals(o3)为true,则o1.equals(o3)也一定为true
- 一致性:如果参与比较的对象没任何改变,则对象比较的结果也不应该有任何改变
- 非空性:任何非空的引用obj,obj.equals(null)的返回值一定为false
5.4 hashCode方法
该方法返回一个int值,该int值是JVM根据对象在内存的中的特征(地址值),通过hash算法计算出的一个结果。
Hash,一般翻译做“散列”,也可以音译为“哈希”,就是把任意长度的数据输入,通过散列算法,变换成固定长度的输出,该输出就是散列值。
一个任意长度的输入转为一个固定长度的输出,是一种压缩映射,也就是说,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。
所以,我们可以认为,Object中的hashCode方法默认返回的是对象的内存地址,但是实际上可能并不是。
public class Object{
public native int hashCode();
}
对于俩个对象的hashCode值:
- 相等的俩个对象,hashCode值一定相等
- hashCode值相同,俩个对象有可能相等,也可能不同等
- hashCode值不同,俩个对象一定不同
对象和它的hashCode值的关系,就相当于人和他的姓氏的关系:
- 相等的俩个人,姓氏一样的相同
- 姓氏相同的俩个人,可能是同一个人(相等),也可能不同的俩个人(不相等)
- 姓氏不同的俩个人,一定是不同的俩个人
思考,hashCode在代码中有什么用?
现在有100个对象,突然又来了一个对象obj,那么怎么判断这个obj对象和之前的100个对象是否有相等的呢?
除了使用obj对象的equals方法和之前的100个对象比较100次之外,是否还有更好的方式?