Java基础系列之类和接口

个人胡说

         类是面向对象的核心概念,在目前我得理解来说,其实就是提供了一种将数据结构和对应的服务封装,而面向对象,除了高深的面向对象思想,再者便是与类相关的一系列相关技术的支持,比如多态等。为什么面向对象编程会成为目前主流的编程?我看来一是这种编程相对于数据流(面向过程编程)来说,面向对象更加符合人类的思维习惯和事物的一些本质,另外就是它强大繁多的库。当然,如果面向过程语言自然也可以实现代码复用、API编程等,但是由于没有封装等概念,所以这样的实现会显得比较撇脚。而提供这些面向对象技术是要付出代价------程序的效率,由于支持了许多的技术和特性,所以效率相对于面向过程语言(特别是C语言)自然无法抗衡,所以,要正确的认识面向对象和面向过程这两种技术,各自适应于不同的领域,就拿C语言和Java语言作为代表,C语言对于执行效率有较高的程序是十分恰当的选择(比如OS、Web服务器、数据库、嵌入式等等),而Java更加适合作为生产语言(Web开发、桌面程序等等)。在这两个不同的层次,我钟爱C语言的简洁、效率、对数据结构的描述能力,又对Java强大的API和生产效率执着,使用C语言编程的时候,我大多数时候都在关注数据结构、数据流等和实际事务处理相关度并不是太大(虽然是事务的底层)的细节,对于整体的编程任务有时候并不能整体的把握(后来我发现如果一开始就设计出相关的API便可以解决很多的问题---自顶向下的设计),使用Java的时候,几乎都在关注事务的处理,对于底层的数据结构只有在高效的算法才会重点关注细节,更多的提高了自己处理事务的效率(因为别人已经帮我做了很多的事情),所以面对C语言和Java语言,在我看来都是影响我一生编程思想的语言。PS:有时候我会想设计出一种新的编程语言,既有C语言的底层能力,又有Java的高层抽象能力(绝对不是C++,我确实不太喜欢C++,至少教科书中的C++我不喜欢),哇,那一定很cool!

 

         Java中的类有class关键字支持,一个public类的名字必须和文件名相统一,不然会编译出错,其生成的.class文件也和主类具有相同的名字(自然和文件名也相同)。

         类是创造对象的模板,相对于类另外的一个代名词就是“封装”,将数据结构和相应的服务结合在一起的机制。也正是这个机制,让类所产生的对象绝大多数的时候更加倾向于提供服务(服务其实就是方法调用),由于服务是基于数据结构,但是在编程的时候,他人更多的时候是想获得某些服务,而对于服务室基于何种数据结构和如何实现关心并不是太多。而没有类就无法做事,所以Java编程的基本任务就是创建各式各样的类。

         类的创建一般是一个名词,类中的属性也是类中的一些名词,相应的动词形成服务。比如,人(Person)这个类,设计出来后其中的属性便有name,sex,age等等,相应的服务便有起床、吃饭、学习、睡觉等等。

 

内部类

         内部类是创建在类内部的类,分为三种:

                   1.静态/非静态成员内部类

                  2.非静态局部内部类

                   3.匿名内部类

         内部类从设计与解决编程问题来看,是使得多重继承更加完善的机制。由于内部类具有类的所有性质,从而可以实现接口和继承非接口类型,而作为类的普通成员,可以毫无阻难的访问外围类的属性和服务,之所以成为内部类是大部分原因是辅助本类解决需要解决的问题,特定的属于某个外围类,所以它虽然是一个成员,却没有必要再去考虑太多其他的事物,比如继承等等。

         每个种类的内部类都具有相应的特点,自然具有不同的用处。如果声明成员类不要求访问外围类的实例,就要始终把static修饰符放在它的声明中。如果一个内部类在单个方法之外是可见的,或者它十分的长,那么就不适合放在方法的内部,而应该作为成员类。

         内部类支持的关键字:.this与 .new 。如果你需要外部对象的引用,就可以使用外部类名字后面紧跟圆点和this,很明显,这个.this是无法用于静态方法或者静态类(只会出现在内部类中(也叫嵌套类))的。另外有一点需要注意的是,普通内部类是不允许声明static成员变量和方法的。.new 关键字则是通过外围类对象生成内部类的对象。相关的代码参考实例可以参考Java集合中HashMap的实现。

         匿名内部类有一个特别的用处,就是实现事件监听器。

         内部类也会生成独立的.class文件,拥有自己的特殊标识符$。

         基本来说,内部类的用法在设计中某些时候能够起到十分巧妙的作用,如何设计好内部类还是经验的问题,Java的接口和内部类结合解决了C++多继承所能解决的问题,相对于C++的多继承,简单且容易理解了许多。

 

类进阶

         与类相关的一个名词叫---信息隐藏或者叫封装,是软件设计的基本原则之一,其最大的原因便是它可以有效的解除组成系统的各个模块之间的耦合关系,使得这些模块可以独立的开发、测试、优化、使用和修改。正式由于这个模块化单独并行开发,所以大大的提高了生产效率。一个系统,如果使用面向对象语言,进行了良好的模块划分,不论是开发效率,还是程序整体的健壮和以维护性都会提高,而类的设计便成为了开发的核心

         设计准则:

                   1.使类和成员的可访问性最小化

类一旦被设计出来,很多的时候对于其使用者便是一种公开的承诺,对于类中的核心数据结构应该隐藏,防止使用者的不小心破坏从而导致服务的错误甚至程序的崩溃。除了静态final域的特殊情况之外,共有类都不应该包含公有域。对于其中的某些可能会被外界访问的数据,应该使用访问方法而不是设置域为公开。有时候你可能会觉得麻烦,但是这一定是你值得做的事情。

        

                   2.使可变性最小化

在设计类的时候,大多数时候都是具体的特定功能类(不是用来被继承的类),这个时候就应该将该类设置为final类,有时候一个类其实只需要一个实例便足够了,单例模式在此时就应该被用来设计这个类。

由于类是不可变的,所以不能被继承从而潜在的造成某些影响,其中的域也尽可能不变,由于不可变域本质上是线程安全的,不要求同步机制。在设计某些可能存在多个线程同时访问的类,线程安全显得特别的重要,比如servlet类,其本质上就是一个遵循单例模式设计的类,没有公有可变域,所以线程不需要去花时间考虑如何使安全。

想让类变得更加可控和final,那么使用静态工厂模式来代替构造器同时私有化类的构造器是一个十分不错的选择。这种方式一个的好处便是可能通过改善静态工厂的算法,从而在后续的改进中增加该类的性能。另外一个好处便是当对类的要求发生变化,可以直接重载静态方法,修改相应的算法,而不用去新创建构造器,由于静态工厂创建对象可以在方法中添加相应的算法和处理,使得创建对象和对类维护的灵活性大大提高。

 

                   3复合优先于继承

继承是面向对象三大技术之一,却打破了类的封装性,且继承也有着诸多的缺点和让人烦恼的地方,比如1.父类的改变导致对子类可能产生影响,而子类越多影响越大,父类的设计必须有足够的眼光和经验才能设计出优秀的父类。2.子类重写父类的方法,向上转型后可能会存在由于对父类中内部方法调用机制不清楚从而导致重写的方法无效甚至错误。3.父类的一些方法的设计缺陷由于各种原因无法修改,子类继承后很有可能依然无法解决,所以会有承担父类API缺陷的风险。在面向对象编程六大原则中,第二条称为“里氏替换原则”便是对类继承的相关约定。

对于继承和组合,只需要问自己一点:“子类需要向上转型吗?”,换句话说,只有当父类和子类存在“is-a”的关系的时候,继承才真正实用。更加明确的一点是:一般用来继承的类是相当容易看出它是用来被继承的。

复合是另外一种类的复用方式,这种方式更加的灵活。比如下面这个例子:希望统计set存放过多少次数据?

                            使用继承

                   import java.util.HashSet;

 

    public class SetExtendsTest<E>extendsHashSet<E>{

 

            private int i = 0;

   

            private static final long serialVersionUID= 1L;

   

            @Override

            public boolean add(E e) {

                i++;

                return super.add(e);

            }

   

            public int getNumber(){

                return i;

            }

    }

                            这个设计确实是可以满足你的要求,你不需要覆盖addAll方法,因为那个方法是间接的使用了add方法,同样,你也可以使用如下的方法

         publicclass AnoterWay<E> extends ForwardingSet<E>{

 

                  private int countNumber = 0;

                  public AnoterWay(Set<E> s) {

                           super(s);

                  }

                  @Override

                  public boolean add(E e) {

                           countNumber++;

                           returnsuper.add(e);

                  }

                  public int getNumebr(){

                           returncountNumber;

                  }

        

         }

         classForwardingSet<E> implements Set<E>{

 

                  private final Set<E> s;

 

                  public ForwardingSet(Set<E> s){

                            this.s = s;

                  }

                  @Override

                  public boolean removeIf(Predicate<?super E> filter) {

                           returns.removeIf(filter);

                  }

                  @Override

                  public Stream<E> stream() {

                           returns.stream();

                  }

         }

                            这里并没有写完所有的方法,其思想便是利用一个类来转发对set的操作请求同时重写add方法,但是这里的set是依据实现了set接口的类,对于任何实现了set接口的类都适合。正是由于AnotherWay将set的操作包装了起来,所以称之为包装器类,也就是设计模式中的包装器模式。这种设计只是一个例子,这里有可能存在过度设计的问题,但是确实,现在的add方法不再依赖具体的类来实现,从而提高了程序的灵活性和健壮性。

                            总体来说,复合相对于继承更加具有优势(当然这里并不是说复合就比继承好了,具体问题具体分析)

                  

                   4.用于继承的类

                            对于专门设计用来继承的类,实际上其设计是十分困难的,正如上面所说,公                   有类一旦发布(特别是用于继承的类),其服务就是对他人的承诺。如果需要设计            专门用于继承的类,务必要尽量的考虑得多一些,但是随之而来的便是对这个类的              限制越来越多,这个也是无法避免的,对于成员和方法,你不能暴露得太多也不能              暴露得太少,其类内部的逻辑关系也基本在以后不会发生变化,由于这个类是用于                   继承的,所以,类的文档就显得十分十分的重要(对于程序文档有一句格言:好的                   API文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的)。相            对于这个被用来继承的类,还有一点需要注意的便是-----不要让构造器调用可能被              重写的方法。

 

                   public class ConstructorTest {

         public ConstructorTest(){

            test();

         }

         public void test(){

            System.out.println("A");

         }

      }

      classConstruct extendsConstructorTest{

         @Override

         public void test() {

            System.out.println("B");

         }

      }

 

                        如果你newConstruct对象,那么打印的结果是两个B,这个已经是十分不错 的结果了,由于继承中创建对象的方式,导致子类的对象创建晚于父类对象的创建,但是子类却重写了父类的某个方法,父类的构造器调用了这个方法,由于多态,导致父类在子类的重写方法被调用,但是,子类目前却还没有完成对象的创建,试想,如果重写方法中使用了子类的某个成员,那么这个程序很有可能会出现无法估计的错误。

                            另一方面,对于那些设计目的并不是用来继承的类来说,就有必要使他们禁止              被子类化!

 

接口(interface)

         Java的接口是Java实现多继承的一个特性,虽然如此说,但是由于接口不提供任何方法的实现,所以在实际的编码过程中可能会存在方法代码的冗余,接口可以继承(叫继承比较合适吧)其他接口(通过implements关键字)。其中的域是public static final的,方法是public的,即使你不这么声明,它是默认的。

         《Java编程思想》中有这么一句话(同时也是设计模式的核心准则):“复用代码的第一种方式就是程序员遵循接口编写他们自己的类”,这是面向对象编程的原则之一-----面向接口编程!设计准则中的依赖倒置原则

         具体的依赖倒置定义是:

                   1、高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
              2
、抽象不应该依赖于细节,细节应该依赖于抽象。

       其中的细节说的是类的具体实现,而抽象当然就是抽象类或者接口

         在对于依赖倒置原则的描述中“其被称为面向对象设计的标志,用哪种语言来编写程序并不重要,如果编写是考虑的是如何针对抽象编程而不是细节编程,即程序中的依赖关系都是终止于抽象类或者接口,就是面向对象程序设计,反之便是过程化程序设计”。其实在小型的项目中,对于依赖倒置并不会体现到十分的明显,但是对于稍大的项目,涉及到3个以上的子系统,那么不同的子系统会分成各种不同的模块,子系统内部模块的相互依赖,子系统间的相互依赖就会比较的复杂,还不说各个模块内部的类相互之间的依赖,当出现需求变化(可能是由于真实的需求变化或者技术更新再或者开发者水平变高)想修改一下依赖关系(不论是方法级别的修改还是类级别的修改),系统越大,如果设计不好,那么维护将会相比开发付出更多的代价,所以,在大型项目中存在着这样一个名言“设计比实现难!”。

         接口设计的另外一个原则便是“接口隔离原则”,其基于依赖倒置原则之上提出的原则。如果类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。

 Java基础系列之类和接口

         这个时候就应该将接口I分离成更小的接口,如下图

Java基础系列之类和接口

         从而遵循了接口分离原则的基本原则:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

采用接口隔离原则对接口进行约束时,要注意以下几点:

  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度
  • 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

         相对于类设计准则中的第四点,接口由于是专门用来被实现的,而且必须实现接口中所有规定的方法,所以接口一旦设计出来并被发布出去,那么基本上来说就是一个永久的承诺,所以对于接口的设计,要反复斟酌,这里由于水平有限,所以不能讲到更深的层次。

         有关更多详细的设计方面的艺术,参考设计模式相关的书籍,Java中的接口是Java的核心,但是设计这条路要求更多的经验,所以,如果走到这一步了,基本就是架构师级别了。

Java基础系列之类和接口,布布扣,bubuko.com

Java基础系列之类和接口

上一篇:Java基础系列之基本控制流


下一篇:C++输入不定长字符串