关于方法重写,有以下规则:重写方法的参数列表应该与原方法完全相同;返回值类型应该和原方法的返回值类型一样或者是它在父类定义时的子类型;重写方法访问级别限制不能比原方法高。例如:如果父类方法声明为公有的,那么子类中的重写方法不能是私有的或是保护的。具体限制级别参考访问修饰符;只有被子类继承时,方法才能被重写;方法定义为 final,将不能被重写(final 关键字将在本节后面讲到);一个方法被定义为 static,将使其不能被重写,但是可以重新声明;一个方法不能被继承,那么也不能被重写;和父类在一个包中的子类能够重写任何没有被声明为 private 和 final 的父类方法;和父类不在同一个包中的子类只能重写 non-final 方法或被声明为 public 或 protected 的方法;一个重写方法能够抛出任何运行时异常,不管被重写方法是否抛出异常。然而重写方法不应该抛出比被重写方法声明的更新更广泛的已检查异常。重写方法能够抛出比被重写方法更窄或更少的异常;构造方法不能重写。
Java 中的方法重写(Overriding)是说子类重新定义了父类的方法。方法重写必须有相同的方法名,参数列表和返回类型。覆盖者访问修饰符的限定大于等于父类方法。而方法重载(Overloading)发生在同一个类里面两个或者是多个方法的方法名相同但是参数不同的情况。
step1:依然是上面的例子,Student 是 People 子类,现在想重写父类的 speak 方法。主菜单:Code -> Override methods 或者右键单击 类 Student 代码块中的任意位置,点击 Generate, 然后选择 Override methods,在弹出列表中选择要重写的方法。step2:单击ok, 生成新的代码。
方法重载是指在一个类中定义多个同名的方法,但要求每个方法具有不同的参数的类型或参数的个数。调用重载方法时,Java 编译器能通过检查调用的方法的参数类型和个数选择一个恰当的方法。方法重载通常用于创建完成一组任务相似但参数的类型或参数的个数或参数的顺序不同的方法。
修改登录方法,当用户输入的用户名和密码正确时,通过 Session 记录登录人信息。然后开发获取登录人员信息方法,返回 Session 中记录的登录人信息。实例:/** * 登录控制器 */@RestControllerpublic class LoginController { /** * 登录方法 */ @RequestMapping("/login") public boolean login(HttpServletRequest request, String username, String password) { if ("imooc".equals(username) && "123".equals(password)) { // 登录成功,则添加Session并存储登录用户名 request.getSession().setAttribute("LOGIN_NAME", username); return true; } return false; } /** * 获取登录人员信息 */ @RequestMapping("/info") public String info(HttpServletRequest request) { return "您就是传说中的:" + request.getSession().getAttribute("LOGIN_NAME"); }}
除了属性可以简写,对象中的方法也是可以简写的,而且更加简洁明了。我们来看下面的例子:const name = '张三'// ES5var person = { name: name, getName: function() { console.log('imooc') }};// ES6var person = { name, getName() { console.log(this.name) }};console.log(person) // {name: "imooc", getName: ƒ}上面的代码中,ES5 中定义一个对象上的方法时需要使用 function 关键字来定义,而 ES6 则直接省略了 冒号和 function 关键字。可以看出使用 ES6 这种简洁的方式更具表达力。在 Node 中进行模块导出时,这种方式更加方便。我们看下面的例子:var person = {};function getName () { return person.name}function setName () { person.name = '李四'}function clear () { person = {};}// ES5 写法module.exports = { getName: getName setName: setName, clear: clear};// ES6写法module.exports = { getName, setName, clear };上面的代码中,我们定义了一个 person 对象,并向外暴露了若干方法用来操作 person 对象,在导出的时候可以看出,ES6 不需要重复地去写变量名,从而更简洁地表达了模块所提供的方法。
例如有两个接口 MyInteface1.java 和 MyInterface2.java,存在相同签名的默认方法:public interface MyInterface1 { default void defaultMethod() { System.out.println("我是MyInterface1接口中的默认方法"); }}public interface MyInterface2 { default void defaultMethod() { System.out.println("我是MyInterface2接口中的默认方法"); }}当实现类实现两个接口时,同名的默认方法将会发生冲突,解决办法是在实现类中重写这个默认方法:public class MyClass implements MyInterface1, MyInterface2 { public void defaultMethod() { System.out.println("我是重写的默认方法"); }}还有一种情况:实现类所继承的父类中也存在与默认方法的同名方法,此时存在三个同名方法:// 声明父类,并在父类中也定义同名方法public class SuperClass { public void defaultMethod() { System.out.println("我是SuperClass中的defaultMethod()方法"); }}// 实现类继承父类,并实现两个接口public class MyClass extends SuperClass implements MyInterface1, MyInterface2 {}实例化 MyClass 类,调用其 defaultMethod() 方法:MyClass myClass = new MyClass();myClass.defaultMethod();此时编译执行,不会报错:我是SuperClass中的defaultMethod()方法实际上,在没有重写的情况下,它执行了实现类的父类 SuperClass 的 defaultMethod() 方法。
例如,在Student类中,有多个study方法:525运行结果:同学真好学!Colorful同学真好学!小慕同学真好学!他今年20岁了代码中的三个study都是重载方法。通常来说,方法重载的返回值类型都是相同的。如果我们在Student类中再增加一个方法:public String study() { return "学习Java语言";}注意,上述的方法不是重载方法,因为我们已经在Student类中定义了无参方法study。判断一个方法是否是重载方法的原则:方法名相同,参数类型或参数个数不同。
5.1.1 声明我们可以使用 default 关键字,在接口主题中实现带方法体的方法,例如:public interface Person { void run(); default void eat() { System.out.println("我是默认的吃方法"); }}5.1.2 调用和重写在实现类中,可以不实现默认方法:public class Student implements Person { @Override public void run() { System.out.println("学生可以跑步"); }}我们也可以在实现类中重写默认方法,重写不需要 default 关键字:public class Student implements Person { @Override public void run() { System.out.println("学生可以跑步"); } // 重写默认方法 @Override public void eat() { // 使用 接口名.super.方法名() 的方式调用接口中默认方法 Person.super.eat(); System.out.println("学生吃东西"); }}如果想要在实现类中调用接口的默认方法,可以使用接口名.super. 方法名 () 的方式调用。这里的 接口名.super 就是接口的引用。5.1.3 使用场景当一个方法不需要所有实现类都进行实现,可以在接口中声明该方法为默认方法;使用默认方法还有一个好处,当接口新增方法时,将方法设定为默认方法,只在需要实现该方法的类中重写它,而不需要在所有实现类中实现。
5.2.1 声明使用 static 关键字在接口中声明静态方法,例如:public interface Person { void walk(); // 声明静态方法 static void sayHello() { System.out.println("Hello imooc!"); }}5.2.2 调用类中的静态方法只能被子类继承而不能被重写,同样在实现类中,静态方法不能被重写。如果想要调用接口中的静态方法,只需使用 接口名。类方法名 的方式即可调用:public class Student implements Person { @Override public void walk() { // 调用接口中的类方法 Person.sayHello(); System.out.println("学生会走路"); }}
如果之前没有学习过设计模式,很可能你的实现会是这样。编写 RecommendMusicService 类,里面有一个 Recommend方法。根据输入的风格不同,执行不同的推荐逻辑。代码如下:public class RecommendMusicService { public List<String> recommend(String style) { List<String> recommendMusicList = new ArrayList<>(); if ("metal".equals(style)) { recommendMusicList.add("Don't cry"); } else if ("country".equals(style)) { recommendMusicList.add("Hotel california"); } else if ("grunge".equals(style)) { recommendMusicList.add("About a girl"); }else { recommendMusicList.add("My heart will go on"); } return recommendMusicList; }}是不是觉得 recommed 方法太长了? OK,我们重构下,把每种音乐风格的推荐逻辑封装到相应的方法中。这样推荐方法就可以复用了。public class RecommendMusicService { public List<String> recommend(String style) { List<String> recommendMusicList = new ArrayList<>(); if ("metal".equals(style)) { recommendMetal(recommendMusicList); } else if ("country".equals(style)) { recommendCountry(recommendMusicList); } else if ("grunge".equals(style)) { recommendGrunge(recommendMusicList); }else { recommendPop(recommendMusicList); } return recommendMusicList; } private void recommendPop(List<String> recommendMusicList) { recommendMusicList.add("My heart will go on"); recommendMusicList.add("Beat it"); } private void recommendGrunge(List<String> recommendMusicList) { recommendMusicList.add("About a girl"); recommendMusicList.add("Smells like teen spirit"); } private void recommendCountry(List<String> recommendMusicList) { recommendMusicList.add("Hotel california"); recommendMusicList.add("Take Me Home Country Roads"); } private void recommendMetal(List<String> recommendMusicList) { recommendMusicList.add("Don't cry"); recommendMusicList.add("Fade to black"); }}这样是不是很完美了!recommend 方法精简了很多,而且每种不同的推荐逻辑都被封装到相应的方法中了。那么,如果再加一种风格推荐怎么办?这有什么难,recommed 方法中加分支就好啦。然后在 RecommendMusicService 中增加一个对应的推荐方法。等等,是不是哪里不太对?回想一下设计模式6大原则的开闭原则----对扩展开放,对修改关闭。面对新风格推荐的需求,我们一直都在修改 RecommendMusicService 这个类。以后每次有新风格推荐要添加,都会导致修改 RecommendMusicService 。显然这是个坏味道。那么如何做到实现新的风格推荐需求时,满足开闭原则呢?
我们知道 Set 是一个集合,其中的元素不能重复,所以可以利用 Set 数据结构的特点进行去重操作。下面给出具体代码:function unique(arr) { return [...new Set(arr)]}上面的代码可以看出,上节我们学习了 Set 和数组直接的转换,可以使用 ... 语法展开 Set 实例就可以得到数组。当然还可以使用 Array.from() 方法把 Set 数据结构转化为数组。这种方式在低版本浏览器是不能运行的。其实上面三种方式都有一定的缺点:第一种方式,时间复杂度高;第二种方式,空间复杂度高;还有一个致命的缺点是,如果数组中的元素是对象形式,那么就不能使用此方法。因为对象的 key 只能是字符串,其解决方式可以使用 Map 数据结构代替 hash 的存储;第三种方式,需要更高级的浏览器。
使用过滤器,当然是绝佳的选择方案,过滤器可以对来来往往的请求包、响应包做统一处理。过滤器以组件独立的方式运行,不需要侵入目标响应控制器组件。完全符合 OOP 的高内聚、低耦合要求。自定义编写一个简单的解决中文乱码过滤器:public class EncodingFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; req.setCharacterEncoding("utf-8"); res.setContentType("text/html;charset=UTF-8"); chain.doFilter(req, res); } @Override public void destroy() { }}编写完过滤器后,需要让 Tomcat 知道它的存在,可以在 web.xml 中进行配置。本章节使用纯 JAVA 的配置方案。打开 WebInitializer 类文件,重写 onStartup()方法,使用 JAVA 方法注册过滤器。@Overridepublic void onStartup(ServletContext servletContext) throws ServletException { super.onStartup(servletContext); EncodingFilter encodingFilter=new EncodingFilter(); FilterRegistration.Dynamic register= servletContext.addFilter("encdoing", encodingFilter); register.addMappingForUrlPatterns( EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE), false, "/*");}Tips: 一定要加上 super.onStartup (servletContext); 语句,必须重用父类中的代码。在此方法中还可以动态配置 Servlet 、监听器 。重构控制器代码:@RequestMapping(value="/login") public String login01(User user) throws UnsupportedEncodingException { System.out.println(user.getUserName()); if("慕课".equals(user.getUserName()) && "123".equals(user.getUserPassword())) { return "index"; } return "fail";}发布项目、重启 tomcat ,打开浏览器,进入登录页面。发送中文登录信息,中文乱码得到解决。
抽象类中可以包含抽象方法,它是没有具体实现的方法。换句话说,与普通方法不同的是,抽象方法没有用 {} 包含的方法体。抽象类可以定义一个完整的编程接口,从而为子类提供实现该编程接口所需的所有方法的方法声明。抽象类可以只声明方法,而不关心这些方法的具体实现,而子类必须去实现这些方法。上面我们将 Pet 父类改为了抽象类,其中包含了 eat 方法的具体实现,而实际上这个方法是不需要具体实现的,每种宠物都有各自的具体实现。此时,就可以将 eat 方法改为抽象方法:abstract class Pet { abstract void eat();}我们可以看到抽象方法使用 abstract 关键字声明,它没有方法体,而直接使用 ; 结尾。子类必须实现父类中的抽象方法,假如 Dog 类继承了抽象类 Pet,但没有实现其抽象方法 eat:class Dog extends Pet {}编译执行代码,将会报错:Dog.java:1: 错误: Dog不是抽象的, 并且未覆盖Pet中的抽象方法eat()public class Dog extends Pet { ^1 个错误上述报错中可知,如果我们不想在 Dog 中重写抽象方法 eat(),那么可以将 Dog 也改为抽象类:abstract class Dog extends Pet {}抽象方法不能是 final、static 和 native 的;并且抽象方法不能是私有的,即不能用 private 修饰。因为抽象方法一定要被子类重写,私有方法、最终方法以及静态方法都不可以被重写,因此抽象方法不能使用 private、final 以及 static 关键字修饰。而 native 是本地方法,它与抽象方法不同的是,它把具体的实现移交给了本地系统的函数库,没有把实现交给子类,因此和 abstract 方法本身就是冲突的。
Java语言本身的类也定义了很多方法重载的例子,例如String类的substring方法,用于字符串截取:public String substring(int beginIndex); // 截取并返回从beginIndex位置到结束位置的字符串public String substring(int beginIndex. int endIndex); // 截取并返回从beginIndex位置到endIndex-1位置的字符串如下为实际应用的实例:String hello = "Hello, Imooc";String substring1 = hello.substring(7);String substring2 = hello.substring(0, 5);System.out.println(substring1);System.out.println(substring2);运行结果:ImoocHello
本节课也要到说再见的时候,留一个问题给大家:如果在持久化类中重写 equals 方法,为什么也要求重写 hashCode 方法。答案其实就在于你对这两个方法的理解了。好了!本节课,和大家一起使用模板设计模式封装了 Hibernate 的操作代码。让 Hibernate 的使用过程变得更简单,更是为了适应真实项目需求。本节课程也和大家一起聊到了持久化对象为什么要实现序列化接口。
本小节我们将学习什么是方法、如何自定义方法,并按照分类介绍每种方法的特点,对于有参数的方法传值,会讲到基本数据类型作为方法参数和引用数据类型作为方法参数的区别。也会学习可变参数方法的定义语法和使用场景,方法重载的使用和意义也是本节的重点学习内容。
使用 hash 方法去重是利用对象的键是唯一的,维护一个以数组中元素为 hash 表的键,由于键是唯一的,所以数组中相同的元素在 hash 表中只会有一个键。 function unique(arr){ const newArr = []; const hash = {}; for(var i=0; i<arr.length; i++){ if(!hash[arr[i]]){ hash[arr[i]] = true; newArr.push(arr[i]); } } return newArr; }上面的代码时间复杂度是 n,只需要对数组进行一次循环即可。把循环的元素存放在 hash 表中来记录不重复的元素。如果 hash 表中找不到对应的值则在 hash 表中添加一个记录,并把该元素 push 到数组中。这样的方式时间复杂度为 n,但是维持一个 hash 表需要更多的空间。
获取到了注解以及其内容,我们就可以编写一个校验方法,来校验字段长度是否合法了。我们在Student类中新增一个checkFieldLength()方法,用于检查字段长度是否合法,如果不合法则抛出异常。完整实例如下:import java.lang.reflect.Field;public class Student { // 标注注解 @Length(min = 2, max = 5, message = "昵称的长度必须在2~5之间") private String nickname; public Student(String nickname) { this.setNickname(nickname); } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public void checkFieldLength(Student student) throws IllegalAccessException { // 遍历所有Field for (Field field: student.getClass().getDeclaredFields()) { // 获取注解 Length annotation = field.getAnnotation(Length.class); if (annotation != null) { // 获取字段 Object o = field.get(student); if (o instanceof String) { String stringField = (String) o; if (stringField.length() < annotation.min() || stringField.length() > annotation.max()) { throw new IllegalArgumentException(field.getName() + ":" + annotation.message()); } } } } } public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { Student student = new Student("小"); student.checkFieldLength(student); }}运行结果:Exception in thread "main" java.lang.IllegalArgumentException: nickname昵称的长度必须在2~5之间 at Student.checkFieldLength(Student.java:32) at Student.main(Student.java:41)运行过程如下:
除过上面代码中使用的最基本的 add (int)、sum () 方法之外,我们再介绍两个方法的使用。reset () 方法将累加器值置为 0,即为后继使用重新归位。sumThenReset () 方法此方法逻辑等同于先调用 sum () 方法再调用 reset () 方法,简化代码编写。
本小节我们介绍了 Java String类的常用方法:使用length()方法可以获取字符串长度;使用charAt()、indexOf()以及lastIndexOf()方法可以对字符串进行查找;substring()方法可以对字符串的进行截取,split()、getBytes()方法可以将字符串切割为数组;toLowerCase()和toUpperCase()方法分别用于大小写转换,使用equals()方法对字符串进行比较,这里要注意,对字符串内容进行比较时,永远都不要使用==运算符。这些方法大多有重载方法,实际工作中,要根据合适的场景选用对应的重载方法。当然,本小节还有很多未介绍到的方法,使用到可以翻阅官网文档来进行学习。
3.2.1 标准方法方法名备注abort中止请求getAllResponseHeaders返回所有用 CRLF 分隔的响应头的字符串的形式。没有收到响应则返回 null。getResponseHeader返回指定响应头的字符串。未收到响应,或者响应不存在该报头,返回 null。open初始化一个请求。overrideMimeType重写 MIME 类型。send发送请求。setRequestHeader设置 HTTP 请求头。3.2.2 非标准方法方法名备注openRequest初始化一个请求。
当父类中方法不希望被重写时,可以将该方法标记为 final:class SuperClass { public final void finalMethod() { System.out.println("我是final方法"); }}class SubClass extneds SuperClass { // 被父类标记为final的方法不允许被继承,编译会报错 @Override public void finalMethod() { }}编辑执行,将会报错:SubClass.java:4: 错误: SubClass中的finalMethod()无法覆盖SuperClass中的finalMethod() public void finalMethod() { ^ 被覆盖的方法为final1 个错误
我们给 name 绑定了一个 handler 方法,之前我们写的 watch 方法其实默认写的就是这个handler。当 name 发生改变时, handler 方法就会执行。579代码解释:第 7-11 行,我们定义了侦听器 name。它是一个对象,当 name 发生变化的时候,会调用 handler 的方法。。
StringBuilder 类下面也提供了很多与 String 类相似的成员方法,以方便我们对字符串进行操作。下面我们将举例介绍一些常用的成员方法。3.2.1 字符串连接可以使用 StringBuilder 的 StringBuilder append(String str) 方法来实现字符串的连接操作。我们知道,String 的连接操作是通过 + 操作符完成连接的:String str1 = "Hello";String str2 = "World";String str3 = str1 + " " + str2;如下是通过 StringBuilder 实现的字符串连接示例:708运行结果:Hello World由于 append() 方法返回的是一个 StringBuilder 类型,我们可以实现链式调用。例如,上述连续两个 append() 方法的调用语句,可以简化为一行语句:str.append(" ").append("World");如果你使用 IDE 编写如上连接字符串的代码,可能会有下面这样的提示(IntelliJ idea 的代码截图):提示内容说可以将 StringBuilder 类型可以替换为 String 类型,也就是说可以将上边地代码改为:String str = "Hello" + " " + "World";这样写并不会导致执行效率的下降,这是因为 Java 编译器在编译和运行期间会自动将字符串连接操作转换为 StringBuilder 操作或者数组复制,间接地优化了由于 String 的不可变性引发的性能问题。值得注意的是,append() 的重载方法有很多,可以实现各种类型的连接操作。例如我们可以连接 char 类型以及 float 类型,实例如下:709运行结果:小明的身高为:172.5上面代码里连续的两个 append() 方法分别调用的是重载方法 StringBuilder append(char c) 和 StringBuilder append(float f)。3.2.2 获取容量可以使用 int capacity() 方法来获取当前容量,容量指定是可以存储的字符数(包含已写入字符),超过此数将进行自动分配。注意,容量与长度(length)不同,长度指的是已经写入字符的长度。例如,构造方法 StringBuilder() 构造一个空字符串生成器,初始容量为 16 个字符。我们可以获取并打印它的容量,实例如下:710运行结果:str的初始容量为:16连接操作后,str的容量为343.2.3 字符串替换可以使用 StringBuilder replace(int start, int end, String str) 方法,来用指定字符串替换从索引位置 start 开始到 end 索引位置结束(不包含 end)的子串。实例如下:711运行结果:Hello Java!也可使用 StringBuilder delete(int start, int end) 方法,先来删除索引位置 start 开始到 end 索引位置(不包含 end)的子串,再使用 StringBuilder insert(int offset, String str) 方法,将字符串插入到序列的 offset 索引位置。同样可以实现字符串的替换,例如:StringBuilder str = new StringBuilder("Hello World!");str.delete(6, 11);str.insert(6, "Java");3.2.4 字符串截取可以使用 StringBuilder substring(int start) 方法来进行字符串截取,例如,我们想截取字符串的后三个字符,实例如下:712运行结果:str截取后子串为:慕课网如果我们想截取示例中的” 欢迎 “二字,可以使用重载方法 StringBuilder substring(int start, int end) 进行截取:String substring = str.substring(3, 5);3.2.5 字符串反转可以使用 StringBuildr reverse() 方法,对字符串进行反转操作,例如:713运行结果:str经过反转操作后为:avaJ olleH
有时候我们不想完全重写父类方法,可以使用 super 关键字调用父类方法,调用父类方法的语法为:super.方法名(参数列表)例如,Cat 类调用父类 Pet 的 eat 方法:class Pet { public void eat() { System.out.println("宠物吃东西"); }}class Cat extends Pet{ public void eat() { // 在 eat 方法中调用父类 eat 方法 super.eat(); System.out.println("小猫饭量很小"); }}class Test { public static void main(String[] args) { Cat cat = new Cat(); cat.eat(); }}运行结果:宠物吃东西小猫饭量很小
在前面我们已经了解过方法的概念,Java 程序的入口main()就是一个方法。System.out.println();语句中println()也是一个方法。如果你使用IntelliJ IDEA查看代码,可以使用Ctrl + 鼠标左键单击代码中的 println()方法,查看 JDK 源码中对于这个方法的定义:上面的截图就是我们经常调用的老朋友println()方法的代码实现,我们可以将方法理解为一个命名语句块,通过其名称 + 括号运算符()可以调用。我们可以将需要重复编写的代码,封装到一个方法中。提高代码的复用性。
SocketChannel 提供了写入单片数据的方法,声明如下:public abstract int write(ByteBuffer src) throws IOException其实,单片数据的 write 方法是重写了 java.nio.channels.WritableByteChannel 中的 write 方法。write 方法是从 ByteBuffer 读取数据,写入 I/O 设备中,为此调用者必须将要写出去的数据保存到 ByteBuffer 中。返回值是写入的字节数、0、或者 -1。如果是阻塞式 Channel,write 返回请求写入的字节数 或者 -1;如果是非阻塞式 write 可能会返回 0。SocketChannel 提供了写入多片数据的方法,声明如下:public final long write(ByteBuffer[] dsts) throws IOExceptionpublic final long write(ByteBuffer[] dsts, int offset, int length) throws IOException多片数据的 write 方法是重写了 java.nio.channels.GatheringByteChannel 中的 write 方法。多片数据 write 方法的返回值和单片数据 write 方法的返回值具有相同的含义。多片数据的 write 方法,其实是将保存在不同的 ByteBuffer 中字节流写入 TCP Socket,这些 ByteBuffer 是不同的内存块,通常叫做 Gathering 机制。
upper () 方法将字符串中所有小写字符转换为大写,示例如下:>>> text = 'abc'>>> text.upper()'ABC'