类
一、类的基础
1.类---一种自定义数据类型。
2.与方法内创建局部变量不同,在创建对象的时候,所有的实例变量都会分配
一个默认值,这与创建数组的时候是类似的。
3.在{}对实例变量内赋值:
int x; int y; { x = 1; y = 2; }
在新创建一个对象的时候会先调用这个初始化,然后再执行构造函数。
静态变量可使用static{}初始化:
private static int STATE_ONE; private static int STATE_TOW; static { STATE_ONE = 1; STATE_TOW = 2; }
静态初始化代码块在类加载的时候执行,这是任何对象创建之前,且只执行一次。
4.对象和数组一样有两块内存,保存地址的部分分配在栈中,而保存实际内容的部分
分配在堆中。
5.jar包:
打包的是编译后文件,将多个编译后文件打包,方便其他程序调用。
到编译后的class文件根目录,运行:
jar -cvf <包名>.jar <最上层包名>
如何使用jar包:将其加入classpath中即可。
二、类的继承
1.使用继承的好处:一是可以复用代码,二是不同的子类对象可以方便地统一管理。
2.向上转型:转换为父类的类型。
3.多态:一种类型变量可以引用多种实际类型对象。
4.动态绑定:在调用时实际调用的是子类的方法。
5.多态和动态绑定是计算机程序的一种重要的思维方式,使得操作对象的程序
不需要关心对象的实际类型。
6.重载:指方法名称相同但参数签名(参数的个数、类型或者顺序)不同。
7.重写:子类重写父类参数签名相同的方法。
8.注意:当有多个重名函数时,在决定调用哪个函数的过程中,首先是按照参数
类型进行匹配,换句话说,先寻找所有重载中最匹配的,然后再看变量的动态类型
进行动态类型绑定。
9.一个父类的变量是否能转换为子类的变量,取决于这个父类的
动态类型(即引用的对象类型)是不是这个子类或者该子类的子类。
10.重写时子类的方法不能降低父类方法的可见性。
11.用final关键子修饰的类不可被继承。
继承实现的基本原理:
举个例子:
基类:
public class Base { public static int s; private int a; static { System.out.println("基类静态代码块, s = " + s); s = 1; } { System.out.println("基类实例代码块, a = " + a); a = 1; } public Base() { System.out.println("基类构造方法, a = " + a); a = 2; } protected void step() { System.out.println("base s = " + s + " a = " + a); } public void action() { System.out.println("start:"); step(); System.out.println("end:"); } }
子类:
public class Child extends Base{ public static int s; private int a; static { System.out.println("子类静态代码块, s = " + s); s = 10; } { System.out.println("子类实例代码块, a = " + a); a = 10; } public Child() { System.out.println("子类构造方法, a = " + a); a = 20; } protected void step() { System.out.println("child s = " + s + " a = " + a); } }
调用:
public class Use { public static void main(String[] args) { System.out.println("-----new Child()"); Child c = new Child(); System.out.println(" -----c.action()"); c.action(); Base b = c; System.out.println(" ------b.action()"); b.action(); System.out.println(" ------c.s: " + c.s); System.out.println(" ------b.s: " + b.s); } } /*-----new Child() 基类静态代码块, s = 0 子类静态代码块, s = 0 基类实例代码块, a = 0 基类构造方法, a = 1 子类实例代码块, a = 0 子类构造方法, a = 10 -----c.action() start: child s = 10 a = 20 end: ------b.action() start: child s = 10 a = 20 end: ------c.s: 10 ------b.s: 1*/
过程详解:
1)类加载过程:
在Java中所谓的类加载是指将类的相关信息加载到内存中。在Java中,类是动态
加载的,当第一次使用这个类的时候才会加载,加载一个类时会查看其父类是否
加载,如果没有则会加载其父类。
存放类信息的内存区域在Java中被称为方法区(不同于堆和栈)。加载后方法区
就有了类的信息:
实例初始化代码包括了实例初始化代码块和构造方法。
本例中,类的加载大概在内存形成了类似上面的布局,然后分别执行了Base和Child的初始化代码。
2)对象的创建过程
类加载过后,new Child()就是创建Child实例。创建过程包括
1.分配内存
2.对所有实例变量赋予默认值
3.执行实例初始化代码
其中,分配的内存包括本类和所有父类的实例变量,但不包括任何类变量。
实例初始化代码从父类开始,再执行子类。
每个对象除了保存有实例变量外,还保存有类信息的引用。
3)方法调用过程
寻找要执行的实例方法的时候,先从对象的实例开始查找,找不到再查找父类。(这也是动态绑定的实现原理)
4)变量的访问过程
变量的访问是静态访问的,无论是类变量还是实际变量。比如例子中,如果变量a不是私有的
b.a访问的是Base中的变量,c.a访问的Child实例中的变量。
继承是一把双刃剑:
继承会破坏封装,而封装可以说是程序设计的基本原则,另外继承可能没有反应出is-a关系。
1.继承会破坏封装
继承可能破坏封装是因为子类和父类可能存在着实现细节的依赖。
子类在继承父类的时候,往往不得不关注父类的实现细节,而父类
如果在修改其内部实现的时候,如果不考虑子类,也往往会影响到子类。
举个例子:
public class Base { private static final int MAX_NUM = 1000; private int[] arr = new int[MAX_NUM]; private int count; public void add(int number) { if (count < MAX_NUM) { arr[count++] = number; } } public void addAll(int[] numbers) { for (int num: numbers) { add(num); } } }
public class Child extends Base{ private long sum; @Override public void add(int number) { super.add(number); sum += number; } @Override public void addAll(int[] numbers) { super.addAll(numbers); for (int i = 0; i < numbers.length; i++) { sum += numbers[i]; } } public long getSum() { return sum; } }
public class Use { public static void main(String[] args) { Child child = new Child(); child.addAll(new int[]{1, 2, 3}); System.out.println("The sum is " + child.getSum());//The sum is 12 } }
说明:如果子类不知道父类的实现细节就不能正确地扩展。子类和父类之间是细节依赖的。
因此,父类不能随意增加公开的方法,因为给父类增加等于给子类增加,而子类可能需要
重写该方法才能保证其正确性。
2.继承没有反应is-a关系
继承关系是用来反应is-a关系的,子类是父类对象的一种,
父类的属性和行为也适用于子类。在is-a关系中,重写方法
时,子类不应该改变父类的预期行为,但这在Java的继承中
是无法约束的。
3.如何应对继承的双面性
方法一:避免使用继承
1)使用final避免继承:
final方法不能被重写,final类不能被继承。
给方法添加final,父类就保留了随意修改该方法内部实现的自由。类添加final同理。
2)优先使用组合而不是继承:
使用组合可以避免父类变化对子类的影响,从而保护子类。
public class Child extends Base{ private Base base; private long sum; public Child(){ base = new Base(); } public void add(int number) { base.add(number); sum+=number; } public void addAll(int[] numbers) { base.addAll(numbers); for(int i=0;i<numbers.length;i++){ sum+=numbers[i]; } } public long getSum() { return sum; } }
3)使用接口
方法二:正确使用继承
三大场景
1)父类是别人写的,我们写子类
我们需要注意:
重写方法不要改变预期行为
阅读文档说明,理解可重写方法的实现机制,尤其是方法间的依赖关系
基类如果修改,阅读其修改说明
2)我们写基类,别人写子类
使用继承反应真正的is-a关系,只将正则公共的部分放入基类。
对不希望被重写的公开方法添加final修饰符
写文档,对子类如何实现提供指导
写修改说明
三、内部类
一个类可以放在另一个类的内部,该类称为内部类,包含它的类称为外部类。
一般而言,内部类与包含它的外部类有密切的关系,而与其他类关系不大,使用内部类,
可以对外部完全隐藏,因此可以有更好的封装性,代码实现上也更为简洁。
不过,内部类知识Java编译器的概念,对于Java虚拟机而言是不知道有内部类这回事的,
每个内部类都被编译为一个独立的类,生成一个独立的字节码文件。
1.静态内部类
public class Outer { private static int shared = 100; //静态内部类只能访问外部类的静态变量和方法,实例变量和方法不能访问。 public static class StaticInner { public void innerMethod() { System.out.println("I got the outer shared=" + shared); } } public void test() { //在外部类内部,可以直接使用静态内部类 StaticInner staticInner = new StaticInner(); staticInner.innerMethod(); } }
//public静态内部类可以被外部使用 Outer.StaticInner inner = new Outer.StaticInner(); inner.innerMethod(); //I got the outer shared=100
静态内部类的实现:
代码实际上会生成两个类,一个是Outer,一个是Outer$StaticInner
public class Outer { private static int shared = 100; public void test() { Outer$StaticInner staticInner = new Outer$StaticInner(); staticInner.innerMethod(); }
static int access$0(){
return shared;
} } public static class Outer$StaticInner { public void innerMethod() { //内部类访问了外部类的静态私有变量,类的私有变量是不能被外部访问到的 //Java的解决办法:自动为Outer生成一个非私有访问方法access$0,它返回shared变量。 System.out.println("I got the outer shared=" + Outer.access$0()); } }
静态内部类使用场景:与外部类关系密切,且不需要使用外部类实例变量。
2.成员内部类 成员内部类没有static修饰
public class Outer { private static int a = 100; public class Inner { public static final int A = 1; // ok //public static int b = 2; 报错 //除了外部类的静态变量和方法,成员内部类可以访问实例变量和方法 public void innerMethod() { System.out.println("I got the outer a = " + a); action(); //如果内部类有方法与外部类重名可以使用 //Outer.this.action(); } } private void action() { System.out.println("action"); } public void test() { Inner inner = new Inner(); inner.innerMethod(); } }
Outer outer = new Outer(); //与静态内部类不同,成员内部类总是与一个外部类的实例关联 Outer.Inner inner = outer.new Inner(); //Outer.Inner inner1 = new Outer.Inner(); 编译器报错
与静态内部类不同,成员内部类里面不能定义静态变量和方法,final修饰的除外。(想一想,WHY????)
成员内部类的实现,也会生成两个类:
public class Outer { private static int a = 100; public void action() { System.out.println("action"); } public void test() { Outer$Inner inner = new Outer$Inner(); inner.innerMethod(); } static int access$0(Outer outer) { return outer.a; } static void access$1(Outer outer) { outer.action(); } } public static class Outer$Inner { final Outer outer; public Outer$Inner(Outer outer) { this.outer = outer; } public void innerMethod() { System.out.println("outer a " + Outer.access$0(outer)); Outer.access$1(outer); } }
成员内部类应用场景:内部类与外部类关系密切,需要访问外部类的实例变量或者方法。
另外,外部类的一些方法的返回值可能是某些接口,为了返回这个接口,外部类方法可能使用
内部实现这些接口,这个内部类可以设置为private,完全对外隐藏。(不需要其他类使用的接口)
3.方法内部类
public class Outer { private int a =100; public void test(final int param) { final String str = "hello"; //方法内部类只能在定义它的方法内使用 //如果该方法是实例方法,则除了静态变量和方法外 //内部类还能直接访问外部类的实例变量和方法 //如果该方法是静态方法,则只能外部类的访问静态变量和方法 class Inner { public void innerMethod() { System.out.println("outer a " + a); System.out.println("param " + param); System.out.println("local str " + str); } } Inner inner = new Inner(); inner.innerMethod(); } public static void main(String[] args) { Outer outer = new Outer(); outer.test(88); } }
方法内部类的实现:
public class Outer { private int a =100; public void test(final int param) { final String str = "hello"; Inner inner = new Inner(this, param); inner.innerMethod(); } static int access$0(Outer outer) { return outer.a; } public class Inner { Outer outer; int param; public Inner(Outer outer, int param) { this.outer = outer; this.param = param; } public void innerMethod() { System.out.println("outer a " + Outer.access$0(this.outer)); System.out.println("param " + param); //String str 并没有作为参数传递,这是因为它被定义为了常量,可直接使用 System.out.println("local str " + "hello"); } } public static void main(String[] args) { Outer outer = new Outer(); outer.test(88); } }
以上代码也解释了为什么方法内部类访问外部方法的参数和局部变量必须定义为final:
因为内部方法类操作的实际是自己的实例变量,并不是外部变量,只是这些变量和外部
变量有一样的值。所以对这些变量赋值,并不会改变外部方法变量的值,
为了避免混淆干脆将外部方法的局部变量和参数声明为final。
如果确实需要修改外部方法的局部变量和参数,可以把他们声明为数组:
public class Outer { private int a =100; public void test(final int param) { final String[] str = new String[] {"hello"}; class Inner { public void innerMethod() { str[0] = "hello world"; } } } }
4.匿名内部类
创建对象的时候定义的类:
四、枚举
枚举可以定义在一个单独的文件中,也可以定义在类的内部。
public enum Size { SMALL,MEDIUM,LARGE } Size size = Size.LARGE; System.out.println(size); //LARGE
枚举变量可以使用equals和==进行比较。枚举类型都有一个方法
ordinal(),表示枚举时在声明时的顺序,从0开始。另外,枚举类型都实现了Comparable。
在switch内部枚举值不能带枚举类型前缀。
枚举类的实现:枚举类型会被Java编译器转换为一个对应的类,该类继承了java.lang.Enum类
public final class Size extends Enum<Size> { public static final Size SMALL = new Size("SMALL", 0); public static final Size MEDIUM = new Size("MEDIUM", 1); public static final Size LARGE = new Size("LARGE", 2); private static Size[] VALUES = new Size[] { SMALL, MEDIUM, LARGE }; private Size(String name, int ordinal) { super(name, ordinal); } public static Size[] values() { Size[] values = new Size[VALUES.length]; System.arraycopy(VALUES, 0, values, 0, VALUES.length); return values; } public static Size valueOf(String name) { return Enum.valueOf(Size.class, name); } }
应用实例:
public enum Size { //枚举值的定义必须放在最上面 //枚举值写完后必须以分号结尾 //枚举值定义完成后才能写其他代码 SMALL("S", "小号"), MEDIUM("M", "中号"), LARGE("L", "大号"); private String abbr; private String title; private Size(String abbr, String title) { this.abbr = abbr; this.title = title; } public String getAbbr() { return abbr; } public void setAbbr(String abbr) { this.abbr = abbr; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public static Size fromAbbr(String abbr) { for (Size size : Size.values()) { if (size.getAbbr().equals(abbr)) { return size; } } return null; } }
自定义枚举id(思考为什么使用自定义id而不是使用ordinal):
public enum Size { XSMALL(10), SMALL(20), MEDIUM(30), LARGE(40); private int id; private Size(int id) { this.id = id; } public int getId() { return id; } }