开发者学堂课程【高校精品课-上海交通大学 -互联网应用开发技术:Java&JavaEE 2】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/76/detail/15751
Java&JavaEE 2
内容介绍
一、回顾
二、单一职责原则
三、里氏替换原则
四、依赖倒置原则
五、接口分离原则
六、开-闭原
一、回顾
Rute 可不可以写两个,rute写在一起比较好应该集中在一起管理所有的路由,你们已经上过C++的课程可能对于C++内容比较熟悉,把Java中的内容于C++做对比例如c++中函数对应Java中多态,接口的概念。
大家不会Java所以才会讲解Java,那么大家对上节课内容是否有疑问,现在可以花1-2分钟进行解答。
Java中有两种数据类型,一种叫原生数据类型或原始数据类型或者叫基本数据类型,还有一种是使用 class 自定义的数据类型。Class中也有两种例如string其实是一种class,系统中默认定义的 system都是。基本数据类型与其他语言都很接近,包括整数类型:byte,short,int,long;浮点数类型:float,double;布尔类型:boolean;字符类型:char。注意其与C++有区别,c++对于int定义只是规定其位数不小于多少,并不是32位编译器就是32位,64位编译器就是64位。
Long类型定义是不小于int类型就是long类型,可能与int一样并不是int 字节数的二倍。在Java 中不能这样定义,在 Java 中编译一次需要在所有机器上都能执行,所有必须有一个统一的规范即数据类型字节数固定;C++并未固定,需要看编译器如何定义,每一种编译器实现还可能不同。浮点数类型:float,double同样与C++ 中不同是字符树固定;布尔类型在 C++ 中可以使用0表示true,1或2表示false但在 Java中不行必须写true或false。
字符类型字节也不同,8位字节只能表示 ASCII 码 256个但字符有很多,在Java中使用Unicode 编码形式,使用16位字节表示一个字符例如0080。
操作符与C++ 基本一样,有一元操作符,大部分是二元操作符,还有三元操作符(?:)含义都是一样的。其他的比较操作符,赋值操作符也都一样。小问题:移位操作符对于一个int数可以移动多少位?移位32行不行?移位33位行不行?大家可以做一个判断。
下面的表达式语句和语句块与C++ 中写法基本一致,表达式有赋值表达式,逻辑表达式等等,语句块也是靠大括号括起来的,别的没什么。
控制流中有 if ,if else 的嵌套或者使用 switch,case与C++相同语句后使用 break结束,case后是标号,使用标号执行对应的语句,如果想要一直执行可以不在语句后添加 break。再向下是循环,可以有 for,while,do while;while是先判断后执行,do while先执行一遍后判断;已经for循环,for each循环,对集合类进行遍历使用 for each比较方便。
定义类,与c++一样在继承父类是把父类中定义的变量继承下来,私有成员不能继承,例如在子类中定义拓展变量seatHeight,在定义构造器可以调用父类参数,对调用父类中的参数进行设置,把 seatHeight 赋初始值,父类中的方法也已经继承下来了,只要在子类中定义 setHeight 即可;同样定义的类全部是 set 方法,在定义变量时使用 public 可以在外面进行赋值此方法不好,而应该使用 private可以防止在外面直接访问变量进行赋值,做不良操作。
定义完类,可以创建对象。Java中创建对象也是用 new,Java 与 c++ 不同的是没有自动化回收机制所有没有 delete即Java虚拟机可以判断那些对象是没有在被引用可以对其回收,具体的逻辑可以看 JVM 的规范。只需记住创建对象只需要 new 不需要 delete 这点相比较与 c++ 来说比较简单一点。
刚才为什么说使用 private,然后使用 public 或protected 方法进行修改就是控制访问其权限;C++与Java封装有一点差异,java中 public与private 权限与c++相同即要么都可以访问要么只用这个类可以访问,protected 与c++一样即只有它和它的子类可以访问,即如果没有继承此类就不能访问如果继承这个类可以进行访问。
Java中多出一种没有修饰符的,默认是包访问权限即只要在一个包(包实际上是组织代码的方式,就是一个目录)中不需要继承就能访问。在 C++ 中有一种 Java也没有即 friend,友人就是在破坏封装,已经定义某一权限但偏偏说有一例外它是我的朋友它可以对我进行访问,这是在破坏封装在 Java中不好,所有Java中没有友人这一概念。
例如有两个包,两个包中各有两个类,其中有一个是另一个的子类,如果他们之间定义不同的访问权限,它们是怎样的访问方式。
在访问控制时,尽量限制暴露出的东西,无论是类还是一个包或者是将来开发的系统,应尽量限制暴露出的东西,不要所有的方法定义都使用 public,例如把例子中的 private 改为 public 那么别人可以绕过你进行修改,所以在写代码是尽量限制暴露出的东西,如果要暴露也是有限暴露即实在不行才使用 public,无论使用那一种语言编写代码都要这样考虑问题。
Java与c++一样都有一个类成员,类成员即所谓静态变量,不属于任何一个实例是所有实例共享,所有通过类名进行访问也可以通过类名实例访问 myBike,不论使用哪种方式访问都是访问此变量,可以认为此变量是这个类中所有成员共享变量,所以此类归属于这类而不归属于任何一个成员。静态方法不能访问实例方法即 静态方法getNumberofBicycles()不能调用没有 static修饰的方法,getNumberofBicycles可能是在没有创建任何对象情况下调用的方法,而f()是在创建对象情况下的方法,所以不允许getNumberofBicycles调用 f()。
Static will main 必须要有一个对象才能调用非静态的方法,但在开始创建一个对象必须能执行其中的代码,所以必须要有一个不需要调用任何对象就可以直接调用的方法,所以一定整个程序必须要有一个static 的 main 函数从它作为整个程序的入口,这是在 spring but中必须要有 static will main 的原因。
例子:System.out.print(2.00 -1.10);这一行代码会输出什么?输出结果为 0.8999999999,应为使用 IEEE 754浮点标准。注意只要有编程语言存在那么这问题永远存在,在应用层面如果编写 bookstare 的例子,如果商品的单价为demical(数据库中的小数)精确到小数点后两位,那么就表示错误;因为此案例已经告诉你不能使用此方法精确表示价格;价格应该是整数单位为分(ceat),在显示或者处理是以分为单位计算,最后除以100然后把结果显示。这不是Java的问题而是都在使用 754 标准,在任何一个系统中都有这一问题,所以价格千万不要使用浮点数表示而是使用整数表示。
例二:public class BrowserTest{
public static void main(String[]args){
System.out.print("iexplore:")
System.out.println(":maximize");
}
}
这段代码能否被执行?这是case 的标号问题,http:是标号后面是参数,在任何一个语言中直接跳到标号处都不是一个好习惯,尽管支持这样的语言特性应尽量不使用它。
这是关于Java的内容,刚才对照 C++讲解。现在讲解Java中完全面向对象设计。完全面向对象设计有一些原则需要遵守,这些原则首字母放在一起是:SOLID。
二、单一职责原则
在做Java的面向对象需要注意什么,第一个单一职责即在设计一个类时实际上每一个类的职责尽量单一,当修改这个类时只有一个原因即需要修改类的职责,如果类有两个职责那么这个类的写法就有问题。例如设计名为 Rectangle 的类,这个类中有两个方法:一个方法(draw)是绘制它,一个方法(area)是计算它的面积,会有一些应用去用它;例如几何应用会调用 area()方法去计算几何体面积,还有gra应用调用 draw方法绘制,即Rectangle 有两个职责一个是绘制一个是计算那么就认为这个类违反了职责单一原则。假设这个类被几何应用和图形化应用使用,可以发现对于图形化应用来说绘制方法没有发生变化是使用不到的求面积的方发生变化,然后这个类告诉图形化应用说需要更新;同样对于几何应用计算面积的方法不变只是绘制图形的方法发生变化,此时更新与更新后的结果相同;问题出现在这个类职责不单一,有两个类的原因可能使得这个类发生变化,一旦发生变化那么依赖于这个类的方法都要发生变化,这不是我们希望看到的。所以对其处理:Rectangle 只保留一个职责draw,然后图形化应用依赖于Rectangle 进行绘制;Rectangle 可以有一个成员变量叫几何的Rectangle ,其中有 area 方法然后计算几何的应用引用几何的Rectangle ,不在引用Rectangle 。
可以把几何的Rectangle 当作Rectangle 的成员,area发生变化对于几何应用可能类发生变化需要更新但对于图形化应用没有任何影响,同样对于draw图形化应用需要发生变化而几何应用不受影响。所以职责单一告诉说把代码之间的回路解开,并不是封装很多类时只要一个类发生变化就全部发生变化。
几何的Rectangle 与Rectangle 不是子类关系,而是Rectangle 持有几何的Rectangle ;组合永远优先于继承。
三、里氏替换原则
第二个原则叫里氏替换原则,但部分编程语言中(包括Java)子类不等于子类型,子类不等于子类型原因:子类型是可以完全当作父类型使用这叫子类型例如有个函数m要求传递一个a进来,如果创建a的子类a‘然后直接传递到m中是允许的而且代码不能有任何的差异,这是里氏替换原则必须要满足这个原则,但是编程语言中没有要求子类型是子类所以直接替换可能做不到,例如企鹅是不是鸟?
如果鸟中定义一个方法fly那么对于企鹅怎么实现此方法呢,m中要求传递一个 bird类型的b进来,直接在b上调用fly;从编程语法上说企鹅拓展出extead bird是没有问题的,对于企鹅类来说要么覆盖fly要么把fly改写成不能fly,无论是哪一种把企鹅传递进去都会显得非常奇怪即要么企鹅飞起来要么说鸟不能飞。
从语法上讲Java不能控制,实际上这种方法不可取,鸟应分为两个子类一种是会飞的鸟一种是不会飞的鸟,不会飞的鸟包括企鹅等会飞的鸟包括鹰,燕子等,会飞的鸟中才会有fly这个方法。从语法方面不能避免所以鸟都会飞的问题,所以说子类并不是子类型,设计时要合理fly不应该在bird这一层而是在bird子类的canfly这一层,Java中语法不能对其进行约束,可以把fiy放在bird中但在调用时非常奇怪所以需要在设计时需要自己把握,子类与父类要满足里氏替换原则即子类全部可以当作父类使用,这个问题再讲解继承时反复提到例如带边框的矩形如何求面积等等。
子类并不是子类型,在语法方面不能保证子类就是子类型,所以在设计子类时需要注意或者在设计类的继承结构时需要考虑清楚那些是公共属性应放在那一层面。
四、依赖倒置原则
再往下时依赖辗转原则,一些高层的模块不应该依赖于底层的模块,抽象的东西不应该依赖于具体的东西大家都应该依赖于抽象的东西。例如现在有一盏灯,灯上面有一个按钮,按钮(即pull方法)会来回拨动,pull会调用灯的开和关那么此方法设计不合理原因是 现在是对灯的开关进行拨动灯会开和关如果把的换成其他的就不合理了。应该设计为ButtonServer的一个接口,接口不带任何实相(再讲C++是有纯虚函数即不带任何实相的函数),现在Button 依赖于 ButtonServer接口而不依赖于灯,即只有pull就会调用开和关的方法,再ButtonServer接口下是灯或其他的事物需要有具体的逻辑。再Button 构造器中会说接一个ButtonServer接口的构造对象bs,pull方法在bs上调用turnon,turnoff,在 new button 给构造器传递一个lamp就是在控制这个灯,如果传递进去的是炉子或者电饭锅会控制传递进去的炉子。不管怎么说 button 不依赖于具体的灯,同样灯也不依赖于 button对其控制,如果是switch,在 new switch时把lamp 传递进去此时灯就收 switch 方法控制。
所以二者互相之间可以解耦都在依赖于接口ButtonServer,就是依赖的倒置。本来时按钮要依赖于灯,现在倒置不互相依赖都依赖于一个抽象的接口,这是依赖倒置原则即button与lamp解耦,没有说button一定要控制 lamp也可以通过接口控制其他的而lamp也说只要能够操作 turnon,turnoff就可以对 lamp 进行操作
五、接口分离原则
在往下是接口分离,例如现在有一个定时开放的门早上六点到晚上六点,在门上有上锁(lock),解锁(unlock),判断是否开放(lsDoorOpen)这三个与定时并无关系,所以还要使用timeout来完成定时器的功能:时间一到就解锁,这个timeout会有一个 timer去实现它。
如果这么去做那么可能Client会依赖于它不想用到的方法,Timedoor中有lock,unlock,lsDoorOpen为了实现功能让Door去扩展字timeout即Door中会有 timeout 这样一个东西,未来如果有人需要使用 Door 的接口但在door中多出 timeout会显得非常鸡肋而且还有一些潜在的副作用因为不知道timeout到达设定时间后会发生什么。
现在有一个定时开放的门,在门上有上锁(lock),解锁(unlock),判断是否开放(lsDoorOpen)还有有一个到时间会自动打开的功能,所以才会做这样的设计对于此设计Door的职责不单一,其次如果不需要定时的用户依赖于Door那么对于door中的timeout会很畏惧。避免此问题有两种方法,一种是在Door中仍有lock,unlock,lsDoorOpen方法,DoorTimerAdapter实现timeout方法,DoorTimerAdapter可以持有类型的对象即 timedoor有数据成员叫DoorTimerAdapter tda,doorTimeOut方法通过调用 tda 的 timeout 方法,也具备定期开门的功能但是Timer Client与DoorTimerAdapter直接没有关系相互不会干扰,未来还能用door接口创建普通的门,这是一种方法是在timedoor中创建DoorTimerAdapter 然后在调用 timeout。
另一种方法在Java不太容易实现,使用多重继承Timedoor 分别实现两个接口Timer Client和Door,这两个接口之间也没有互相感染,不像刚开始使用的方法。
但在Java中只能单重继承即只能继承一个类,可以把Timer Client变成抽象类,把Door当成接口创建一个接口继承自己的抽象类即可,所以两个接口之间是分离的,这是接口分离原则。
六、开-闭原则
下面是开-闭原则,如果写好一个模块,对模块添加功能是开放的,对于模块进行修改是封闭的,例如在完成一个规模较大的程序,每人分配一个功能,那么在完成功能后只能对模块进行增添功能不能说原功能发生变化然其他人修改功能,原因是你要修改那么其他人也要修改会影响使用此模块的其他人,如果只是对模块增加那么其他人使用此模块不需要修改即可使用因为只是增加功能那么原来使用此模块的功能不需要修改,如果想要使用新功能只需要做对应增加即可无需修改原来的代码,修改代码非常麻烦:修改完后需要对代码进行重新测试,如果原来的代码经过测试无误可以对其冻结。
所以应该对模块的拓展开放对修改关闭。
未来 Clent 与Server 在交互时,server不断修改那么Clent就没有办法固定其自己的代码,解决办法:在协商好后首先定义接口说明接口会提供说明功能,然后对接口编程,轻易不修改接口只能增加接口不能修改原来的接口,所以这种策略下都要依赖于接口,接口是一个抽象程度非常高的东西不含有任何的具体实现所以它是稳定的,只要进行实现就不稳定越具体的实现越不稳定。
例如Clent依赖于数据库主键生成策略Client Interface,可能有不同的策略如基于整数递增等等,具体使用那一功能不管,只知道实现获取下一主键的方法,Client直接调用Client Interface就可以返回一个主键,具体是那一实现将来可以进行替换,可以对接口增加新的功能只要有获取下一主键的方法就可以实现Client方法。
所以针对接口编程非常重要,在讲Java后台开发全是针对接口的编程即Autowied标注完成后全是接口类型而不是实体类型,针对接口编程是我们的追求。大体上是者几个原则。
讨论:
里氏替换原则中例子中是什么并不是所以的子类都能实现fly,所以把fly放在brid下是不对的,有同学说把fly放在brid下,在m函数中调用如果是fly就调用,不是fly不调用,此方法比较麻烦:所以传递brid的地方都要按此方法编写,不然把fly放到canfly中。
如果觉得讲解的不够详细,可以搜索相关的五个原则文件查看,在ppt后也有一个链接对其讲解更加详细。
关于Java问题对照C++进行了讲解,只要掌握一种语言掌握编程思想那么难度就不会太大,更复杂的像嵌套类没有讲解,觉得大家可能目前用不到,未来需要可以自行了解,现在的讲解内容对于编程角度是够用的。