面向对象下篇

前言

上篇我们总结了面向对象中最基本的概念,针对封装、继承、多态也都做了解释,接下里我们进一步看下面向对象中其他的知识点。

Java抽象类 VS Object-c类别

  • 抽象类的引入:我们都知道一般的方法都有具体的方法体,但是有时候某个父类只是知道子类应该包含什么方法,但无法确定这些子类如果实现的这些方法,这时候就用到了抽象类,用关键字abstract来修饰,他可以有抽象方法。有几点说明:
  1. 类和方法都用abstract来修饰,其中方法不能有方法体。
  2. 抽象类不能被实例化,无法使用new关键字来调用构造函数创建实例。
  3. 抽象类可以包含属性、方法(普通方法和抽象方法)、构造器、初始化块、内部类、枚举 等。
  4. 含有抽象方法的类只能被定义成抽象类。
  5. 抽象类体现着一种模板的设计思想,很好的将面向对象抽象的特征展现出来,我们可以把大多数子类的公共部分提取出来作为一个抽象类,然后对其进行改造。
1
2
3
public abstract class Base{
public abstract void baseMethod();
}
  • 类别的引入:因为Object-c中没有引入Java的抽象类,但是有时候需要这一需求,Object-c的做法是在不创建子类的情况下,给现有类额外添加新方法,用来扩展类的能力。:如你不想继承,但是又想给这个类添加新功能,就可以考虑用类别。
1
2
3
4
@interface 已有类 (类别名)
//方法定义
...
@end

需要说明几点:

  1. 类别通常只能定义方法,如果需要添加变量,可以考虑创建子类。
  2. 一个类,可以拥有多个分类。
  3. 和一般接口不同的是,不必实现类别所有的方法,这个对程序的扩展很有用,因为可以在该类类别的声明所有的方法,然后在一段时间后再来实现它。
  4. 通过类别添加新的方法来扩展该类,不仅仅会影响该类,还会影响该类所有的子类,因此他的所有的子类都会继承这个类别,而不管这些子类愿意不愿意。
  5. 类别可以重载该类的另一个方法,但是通常不建议这么做。第一:重载一个方法后,再也不能访问原来的方法,因此,必须小心将被重载方法中的所有功能复制到替换方法中,如果确实需要重载方法,可以考虑创建子类,如果在子类中重载方法,仍然可以通过向super发送消息类来引用父类的方法。因此不必了解重载方法的内容就能够调用父类的方法,并向子类的方法添加自己的功能。
  6. 还有一点要说明的就是如果你用类别的方式重载了Cocoa类,那么你的App就有可能通不过苹果的作品审核,因为苹果对这些自定义的修改的要求是极其严格,甚至说是苛刻的。
  7. 类别的接口文件命名为“类名+类别名.h”,而实现文件命名为“类名+类别名.m”。
  8. 关于类别,我感觉使用在扩展那些你无法看到类的实现代码的类的,你可以为这些类添加扩展函数,可以重载源类中的成员函数,来满足自己的实际需求,但是对于那些你可以看到类的实现、访问的类来说,你则完全没有必要写类别。另外对于那些需要分工合作的工作,也是适合写分类的地方。
  9. 要注意的是Objective-c和Java一样只支持单继承,如果要实现多继承的话,可以通过类别和协议的方式来实现。Java可以用接下来的接口
  10. 类别有3种用法:
    • 利用类别对类进行模块化设计。
    • 使用类别来定义前向引用来调用私有方法。
      上篇文章中提到没有在接口部分定义而在类实现部分定义的方法可以相当于私有方法,通常不允许被调用,但Object-c通常没有真正意义上面的私有方法,可以使用NSObject的performSelector:方法来执行动态调用,但这又完全避开了编译器的检查,也不太好,这里就用上类别了。如上篇文章开头代码中的grade存取方法,我们在其他类中直接调用就会报错,这时我们可以定义一个SLStudent类的类别在它被调用的地方的前面,然后声明这些方法即可。
    • 使用类别来实现非正式协议(后面提到)。
  11. 还有一种就是扩展,相当于匿名类别。
    1
    2
    3
    4
    5
    6
    7
    @interface 已有类 ()
    {
    实例变量
    }
    //方法定义
    ...
    @end

做几点说明:

  1. 类别通常有单独的.h和.m文件,扩展用于临时对某个类接口进行扩展,类实现部分同时实现类接口部分定义的方法和扩展中定义的方法。记得在类实现部分不要忘记导入类扩展部分的.h文件
  2. 类别不可定义属性或成员变量;但扩展可以在类扩展中声明属性和实例变量。

关于类别和类扩展的区别:

  1. 类别中只能增加方法;

  2. 是的,你没看错,类扩展不仅可以增加方法,还可以增加实例变量(或者合成属性),只是该实例变量默认是@private类型的(作用范围只能在自身类,而不是子类或其他地方);

  3. 类扩展中声明的方法没被实现,编译器会报警,但是类别中的方法没被实现编译器是不会有任何警告的。这是因为类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中。

  4. 类扩展不能像类别那样拥有独立的实现部分(@implementation部分),也就是说,类扩展所声明的方法必须依托对应类的实现部分来实现。

  5. 定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。

Java更彻底的抽象:接口 VS Object-c中的协议

Java接口定义:接口更多的时定义一种规范,因此不能包含构造器或者初始化块。

1
2
3
4
5
6
7
8
9
10
11
[修饰符] interface 接口名 extends 父接口1,父接口2...
{
0到多个常量定义...
0到多个抽象方法定义...
}

//使用
[修饰符] class 类名 extends 父类 implements 接口1,接口2...
{
类体部分
}
  1. 修饰符可以用public修饰或者省略(以包访问控制符)
  2. 接口可以多继承,这也是为单继承灵活性不足所做的补充,即只能继承接口,不能继承类
  3. 接口里的属性只能是常量(static final两个修饰符修饰),方法只能是抽象方法。

这里我们拿抽象类与接口做个总结:

  1. 接口和抽象类都不能被实例化。
  2. 接口和抽象类都可以包含抽象方法。
  3. 接口里只能包含抽象方法,不包含已经提供实现的方法,抽象类则可以包含普通方法。
  4. 接口里不能定义静态方法,抽象类里可以定义静态方法。
  5. 接口里只能定义静态常量属性,不能定义普通属性,抽象类里两者都可以。
  6. 接口不包含构造器,抽象类包含,抽象类里的构造器并不是用来创建对象,而让其子类调用这些构造器来完成属于抽象类的初始化操作。
  7. 接口里不能包含初始化块,但抽象类可以。
  8. 一个类最多只有一个直接父类,包括抽象类,但一个类可以实现多个接口,可以实现多个接口来弥补Java单继承的不足。

很多软件设计思想都强调面向接口编程,这样可以降低程序之间的耦合性,我们举个例子:
下面看一个网上流传最广泛的例子:门和警报的例子:门都有open()和close()两个动作,此时我们可以定义通过抽象类和接口来定义这个抽象概念:

1
2
3
4
5
6
7
8
9
10
11
//抽象类
abstract class Door {
public abstract void open();
public abstract void close();
}

//或者接口
interface Door {
public abstract void open();
public abstract void close();
}

但是现在如果我们需要门具有报警alarm( )的功能,那么该如何实现?下面提供两种思路:

  1. 将这三个功能都放在抽象类里面,但是这样一来所有继承于这个抽象类的子类都具备了报警功能,但是有的门并不一定具备报警功能;
  2. 将这三个功能都放在接口里面,需要用到报警功能的类就需要实现这个接口中的open()和close(),也许这个类根本就不具备open()和close()这两个功能,比如火灾报警器。
    从这里可以看出, Door的open() 、close()和alarm()根本就属于两个不同范畴内的行为,open()和close()属于门本身固有的行为特性,而alarm()属于延伸的附加行为。因此最好的解决办法是单独将报警设计为一个接口,包含alarm()行为,Door设计为单独的一个抽象类,包含open和close两种行为。再设计一个报警门继承Door类和实现Alarm接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Alram {
void alarm();
}

abstract class Door {
void open();
void close();
}

class AlarmDoor extends Door implements Alarm {
void oepn() {
//....
}
void close() {
//....
}
void alarm() {
//....
}
}

Object-c协议定义:类似于接口;前面介绍说用类别可以实现非正式协议,当某个类实现NSObject的该类别时,就需要实现该类别下的所有方法,这种基于NSObject定义的类别即可认为是非正式协议。

正式协议定义:使用@protocol关键字。

1
2
3
4
5
6
7
8
//定义
@protocol 协议名 <父协议1,父协议2>
{
0到多个方法定义...
}

//遵守(实现)协议
@interface 类名:父类 <协议1,协议2...>

  1. 协议可以多继承,且只能继承协议,不能继承类
  2. 协议定义的方法只有声明,没有实现。
  3. 父协议继承用<>括号。
  4. 如果程序需要使用协议

如果程序需要使用协议来定义常量,有如下两种语法:

  1. NSObject<协议1,协议2…>* 变量;
  2. id<协议1,协议2…> 变量;
    通过上面两种方式定义的变量,编译时类型仅仅只是所遵守的协议类型,因此只能调用该协议中的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//ProtocolAbility.h文件
#import <Foundation/Foundation.h>

@protocol ProtocolAbility

@required
- (void) eat;

@optional
- (void) write;

@end
-------------------------------------------
//定义MyProtocol.h协议,这个协议遵守ProtocolAbility协议
#import <Foundation/Foundation.h>
#import "ProtocolAbility.h"

@protocol MyProtocol <ProtocolAbility>

@required
- (void) setName;

@optional
- (void) setAge;

@end
-------------------------------------------
//定义新的协议NewProtocol.h
#import <Foundation/Foundation.h>
@protocol NewProtocol

@required
-(NSString *) getname;

@end
-------------------------------------------
//定义SLStudent类实现协议MyProtocol和NewProtocol
//SLStudent.h文件
#import <Foundation/Foundation.h>
#import "MyProtocol.h"
#import "NewProtocol.h"

@interface SLStudent : NSObject <MyProtocol,NewProtocol>

@end
--------------------------------------------
//SLStudent.m文件
#import "SLStudent.h"

@implementation SLStudent

- (void) write{
NSLog(@"write");
}

- (void) eat{
NSLog(@"eat");
}

- (void) setAge{
NSLog(@"Student---setAge");
}

- (void) setName{
NSLog(@"Student----setName");
}

- (NSString *) getname{
return @"宇行信";
}

@end

关于协议相关约束:

  1. @required 用于表示协议中该方法必须在类中实现,默认[如果不加则默认为@required]
  2. @optional 用于表示协议中该方法在类中可以选择实现

关于正式协议和非正式协议的总结:

  1. 非正式协议通过NSObject来创建类别实现,而正式协议则直接使用@protocal创建。
  2. 遵守非正式协议通过带特定类别的NSObject来实现,而遵守正式协议则有专门的Object-c语法。
  3. 遵守非正式协议不要求实现协议中的定义的所有方法,而遵守正式协议则必须实现协议中定义的所有方法。如果协议中使用了以上协议相关约束关键字,则正式协议完全可以代替非正式协议。
  4. conformsToProtocol 方法用于判断某个类是否遵循某个协议,返回值为bool类型,即使协议是通过”继承”过来的也可以。
1
2
bool flag=[student conformsToProtocol:@protocol(NewProtocol)];
NSLog(@"%d",flag);
  1. 关于协议不得不说的时委托的使用,协议是一个<协议名>,委托是个带有协议的对象id <协议名>,这部分会放在iOS开发中来做说明:常见的就是两个页面使用委托来传值。

Java内部类

通常我们把定义在其他类内部的类叫做内部类,有的也叫做嵌套类 ,包含内部类的类叫外部类,有的也叫宿主类;内部类的类名不需要和文件夹相同。

  • 内部类可以是静态static的,也可用public,default,protected和private修饰。(而外部顶级类即类名和文件名相同的只能使用public和default)
  • 内部类是一个编译时的概念,一旦编译成功,就会成为完全不同的两类。对于一个名为outer的外部类和其内部定义的名为inner的内部类。编译完成后出现outer.class和outer$inner.class两类。所以内部类的成员变量/方法名可以和外部类的相同。

成员内部类

成员内部类,就是作为外部类的成员,可以直接使用外部类的所有成员和方法,即使是private的。同时外部类要访问内部类的所有成员变量/方法,则需要通过内部类的对象来获取。成员内部类不能含有static的变量和方法。因为成员内部类需要先创建了外部类,才能创建它自己的,在成员内部类要引用外部类对象时,使用outer.this来表示外部类对象;而需要创建内部类对象,可以使用outer.inner obj = outerobj.new inner();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Outer {
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.print("Outer.new");

inner = outer.getInner();
inner.print("Outer.get");
}

// 个人推荐使用getxxx()来获取成员内部类,尤其是该内部类的构造函数无参数时
public Inner getInner() {
return new Inner();
}

public class Inner {
public void print(String str) {
System.out.println(str);
}
}
}

局部内部类

是指内部类定义在方法和作用域内。Thinking in Java给了这么两个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//定义在方法内
public class Parcel4 {
public Destination destination(String s) {
class PDestination implements Destination {
private String label;

private PDestination(String whereTo) {
label = whereTo;
}

public String readLabel() {
return label;
}
}
return new PDestination(s);
}

public static void main(String[] args) {
Parcel4 p = new Parcel4();
Destination d = p.destination("Tasmania");
}
}

//定义在作用域里:
public class Parcel5 {
private void internalTracking(boolean b) {
if (b) {
class TrackingSlip {
private String id;
TrackingSlip(String s) {
id = s;
}
String getSlip() {
return id;
}
}
TrackingSlip ts = new TrackingSlip("slip");
String s = ts.getSlip();
}
}

public void track() {
internalTracking(true);
}

public static void main(String[] args) {
Parcel5 p = new Parcel5();
p.track();
}
}

嵌套内部类

  1. 嵌套内部类,就是修饰为static的内部类。声明为static的内部类,不需要内部类对象和外部类对象之间的联系,就是说我们可以直接引用outer.inner,即不需要创建外部类,也不需要创建内部类。
  2. 嵌套类和普通的内部类还有一个区别:普通内部类不能有static数据和static属性,也不能包含嵌套类,但嵌套类可以。而嵌套类不能声明为private,一般声明为public,方便调用。

匿名内部类

有时候我为了免去给内部类命名,便倾向于使用匿名内部类,因为它没有名字,在Android开发中我们会经常见到,匿名内部类是不能加访问修饰符的。要注意的是,new 匿名类,这个类是要先定义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
((Button) findViewById(R.id.start)).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread() {

@Override
public void run() {
// TODO Auto-generated method stub
}

}.start();
}
});

//代码
public class Outer {
public static void main(String[] args) {
Outer outer = new Outer();
Inner inner = outer.getInner("Inner", "gz");
System.out.println(inner.getName());
}

public Inner getInner(final String name, String city) {
return new Inner(name, city) {
private String nameStr = name;

public String getName() {
return nameStr;
}
};
}
}

abstract class Inner {
Inner(String name, String city) {
System.out.println(city);
}

abstract String getName();
}

同时在这个例子,留意外部类的方法的形参,当所在的方法的形参需要被内部类里面使用时,该形参必须为final。这里可以看到形参name已经定义为final了,而形参city 没有被使用则不用定义为final。设计为final原因:内部类中如果改掉参数的值,但是外部调用的时候又发现值其实没有被改掉,这就让人非常的难以理解和接受,所以编译器设计人员把内部类能够使用的参数设定为必须是final来规避这种莫名其妙错误的存在。
(简单理解就是,拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变).

枚举类

这里比较简单,我们就不多说。

Java对象的强、软、弱、虚引用

  1. 强引用:
    以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
  2. 软引用(SoftReference):
    如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
    软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。
  3. 弱引用(WeakReference):
    如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
    弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
  4. 虚引用(PhantomReference):
    “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
    虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
  5. 特别注意,在世纪程序设计中一般很少使用弱引用与虚引用,使用软用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
分享到:
移动开发者/技术爱好者/喜欢开源与分享,你也可以关注微信公众号MobDevGroup,移动开发在线分享:MobDevGroup