最适合小白的java接口教程

简介: 控制台输入1,则登录客户信息。这个登录信息的代码,目前全部写在Application.java里面了,这样会带来一个问题,就是程序不好维护。随着业务逻辑越来越复杂,Application.java 就会越来越难懂。所以,要把这部分业务拆分出去。

可能是老板看我太辛苦,就新招聘了一个员工 – 小A。我,小白,作为老员工,肯定要好好带带他。我真的很担心小A的代码不规范,咋办呢?

我把我的想法跟老板说了,老板也很赞同我的想法。因为我属于老员工了,老板也比较信任我。


“小白啊,小A刚来,你要多带带他。这个项目是你一手做起来的,这一点上我很赞同你的想法。你可以做接口设计,然后把具体的实现交给小A去做。”


“接口啊,大学里面好像有教过。”


“是啊,接口是一种特殊的类,里面只有方法的声明而没有方法的实现。不过最新的jdk也允许写方法体了。”


“嗯嗯,我知道的,可是上学那会我就特别不理解接口到底有啥用,不写方法实现,就是一个空壳子哇!”


“嗯,接口的作用还是很大的,这一点你以后会慢慢明白的。小A刚来,代码不规范,逻辑思维还不完善,所以你作为老员工,就需要用接口来规范代码。”


"哦。。。"我似懂非懂地点了点头。


接着,老板又仔细给我讲解了接口的用法,我这才茅塞顿开。原来,用接口能做这么优雅的事情,看来我在学校学的,还有自己理解的,都还只是皮毛啊,与实际工作脱节了。


先不说接口到底啥概念了,先来看看怎么用接口。现在的菜单是这样子的:

a8faf1380c3298f0d1c7b4149f77d0d0.png


控制台输入1,则登录客户信息。这个登录信息的代码,目前全部写在Application.java里面了,这样会带来一个问题,就是程序不好维护。随着业务逻辑越来越复杂,Application.java 就会越来越难懂。所以,要把这部分业务拆分出去。


老板的意思是,我来制定接口,然后让新来的小A来实现,这样的话,项目的大概思路就不至于滑坡。


创建一个service包,然后创建ICustomerService接口,看下面的步骤图。

fc6980a6b2f97131e48a2ca0724c1899.png

307de85a427ce2fea15c1ad1fe7535df.png


注意,下面的Kind是Interface。

ada391294b124421b783aaf58ffffb2b.png

package service;
import entity.Customer;
import java.util.List;
/**
 * 客户服务接口
 * author 小白
 */
public interface ICustomerService {
    //加载文本的客户资料
    public abstract List<String> loadCustomerFormTxt(String path);
    //填充属性到客户对象
    //public 和 abstract可以省略
    Customer fillCustomer(List<String> props);
    /**
     * 保存客户对象到Excel
     * @param cst 客户对象
     * @param excelPath Excel的路径
     * @return
     */
    boolean saveToExcel(Customer cst,String excelPath);
}

接口的定义和类非常像,区别是:

1.接口用的关键字是interface,而不是class

2.接口里面的方法只有声明,没有方法体,这种方法也叫做抽象方法(用abstract关键字修饰)

3.接口里面的方法默认都是public

b3029f3b57cb77cce237af0abc6d96a1.png

因为暂时没有接口的实现类(方法都是空的),所以这边只是引用,不去new。


先把原来的保存代码注释掉,然后依次调用接口的方法,顺便说一句,<preconsolas’;font-size:10.5pt;" style=“box-sizing: border-box; font-size: 15px; font-family: Helvetica;”>cstList做成static的,方便调用。</preconsolas’;font-size:10.5pt;">

static TuziLinkedList cstList = new TuziLinkedList();

main方法源码:

public static void main(String[] args){
      newLine("******欢迎使用兔子餐厅会员系统******");
      newLine("[1]客户登记入库");
      newLine("[9]退出程序");
      //创建一个控制台输入对象,反正就这么写,目前不用去深究含义
      Scanner scanner = new Scanner(System.in);
      //获取用户的输入,程序会挂起等待
      int input = scanner.nextInt();
      //直接退出程序
      if(input == 9){
        newLine("程序退出,谢谢使用!");
        return;
    }
    //自动读取D盘的customer.txt文件,保存入库
    if(input == 1){
//      List<String> lines = FileUtil.readLines(new File("D:/customer.txt"), "GBK");
      Application app = new Application();
      //1、读取文本,获取客户资料
      List<String> props = app.customerService.loadCustomerFormTxt("D:/customer.txt");
      //2、解析客户资料到客户对象
      Customer customer = app.customerService.fillCustomer(props);
      //3、直接将客户对象保存到Excel中(暂时先不做批量入库了)
      boolean isSuccess = app.customerService.saveToExcel(customer,"D:/customer.xls");
      if(!isSuccess){
        System.out.println("保存失败,系统出现异常!");
      }else{
        //4、将这个对象存入链表,备用
        cstList.add(customer);
      }
//      //先创建一个客户对象,就是setFieldValue方法的第一个参数(给谁的属性赋值)
//      Customer cst = new Customer();
//
//      for (String line : lines) {
//        //System.out.println(line);
//        //用字符串的split方法,根据=分割成数组,比如name=张大胖,就会变成['name','张大胖']
//        String[] split = line.split("=");
//        String key = split[0]; //获取数组的第一个元素
//        String value = split[1]; //获取数组的第二个元素
//        //System.out.println(key + "," + value);
//        ReflectUtil.setFieldValue(cst,key,value);
//      }
//
//      //打印顾客对象,就是调用他的toString方法
//      System.out.println(cst);
//      System.out.println(cst.getName() + "保存完毕!");
    }
     //运行完毕后,继续执行main方法,这样程序就不会关闭
     Application.main(null);
   }

代码瞬间清晰了,这就是接口的魅力啊!我只需要负责设计出方法的名字,参数和返回值就可以了,剩下的,我去交给别人做。当然,现在这个程序是不能运行的,因为customerService没有赋值,还是null。


“小A啊,这个接口你帮忙实现一下了哇!”,虽然有点不好意思,但我毕竟是老员工。而且很多业务逻辑已经在main方法里面写了,小A可以去照搬我之前的代码。不过,我也怕小A在别的地方胡乱创建class,所以把实现类也建好了。

f5833d52c16821513f589eb5457e1bdc.png

ebf869afe57e93da0cec70be88380471.png

4b9dc552500611c82790a84b2e96e56e.png

a4210b1f6d12f2498bcb481bccd59442.png


9d7a1a5188102c4d37b5acc99e590cf2.png


package service.impl;
import entity.Customer;
import service.ICustomerService;
import java.util.List;
public class CustomerServiceImpl implements ICustomerService{
    @Override
    public List<String> loadCustomerFormTxt(String path) {
        return null;
    }
    @Override
    public Customer fillCustomer(List<String> props) {
        return null;
    }
    @Override
    public boolean saveToExcel(Customer cst, String excelPath) {
        return false;
    }
}

接口的实现类必须重写所有抽象方法,CustomerServiceImpl既然实现了ICustomerService,就必须重写所有的抽象方法,这是一种约定。既然你打算遵循我设计的接口,就必须实现我要求的所有方法。

@Override
public List<String> loadCustomerFormTxt(String path) {
    return  FileUtil.readLines(new File(path), "GBK");
}

小A基本照搬了我的代码。

@Override
public Customer fillCustomer(List<String> props) {
    //先创建一个客户对象,就是setFieldValue方法的第一个参数(给谁的属性赋值)
    Customer cst = new Customer();
    for (String line : props) {
        //用字符串的split方法,根据=分割成数组,比如name=张大胖,就会变成['name','张大胖']
        String[] split = line.split("=");
        String key = split[0]; //获取数组的第一个元素
        String value = split[1]; //获取数组的第二个元素
        ReflectUtil.setFieldValue(cst,key,value);
    }
    return cst;
}

还是照搬。

@Override
public boolean saveToExcel(Customer cst, String excelPath) {
    System.out.println("不会");
    return true
}


这。。。

我们修改一下Application的代码,给接口对象赋值。

private ICustomerService customerService = new CustomerServiceImpl();


核心代码

Application app = new Application();
//1、读取文本,获取客户资料
List<String> props = app.customerService.loadCustomerFormTxt("D:/customer.txt");
//2、解析客户资料到客户对象
Customer customer = app.customerService.fillCustomer(props);
//3、直接将客户对象保存到Excel中(暂时先不做批量入库了)
boolean isSuccess = app.customerService.saveToExcel(customer,"D:/customer.xls");
if(!isSuccess){
  System.out.println("保存失败,系统出现异常!");
}else{
  //4、将这个对象存入链表,备用
  cstList.add(customer);
}
System.out.print("打印链表---" );
cstList.display();

运行:

5b5bd45732d024fee5e6267cee5c40de.png

听说老板曾经在真正的软件公司工作过,这天我跟他请教企业中怎么体现多态了,我看了下我的代码,改了下,然后让我拿去思考。

我看了一下,改动还是挺大的。


1.主类放在了一个包中,所有的类都不推荐放在默认包。

028d61d9f4edbd0dde13ef936ded75d1.png


2.创建了一个私有方法 saveCustomer,把service对象当做参数传进去。

boolean isSuccess = saveCustomer(app.customerService);


方法如下:

private static boolean saveCustomer(ICustomerService customerService) {
    //1、读取文本,获取客户资料
    List<String> props = customerService.loadCustomerFormTxt("D:/customer.txt");
    //2、解析客户资料到客户对象
    Customer customer = customerService.fillCustomer(props);
    //3、直接将客户对象保存到Excel中(暂时先不做批量入库了)
    boolean isSuccess = customerService.saveToExcel(customer,"D:/customer.xls");
    Application.cstList.add(customer);
    return isSuccess;
  }

目前我对于多态的看法,除了父类引用可以指向子类对象以外,就是这个传参的情况。

boolean isSuccess = saveCustomer(app.customerService);


其实本质还是一样的,因为app.customerService的真实对象,还是CustomerServiceImpl,是子类对象。

我想了很久,终于明白了,多态的好处就是:代码未动,设计先行!

比如这个例子,我只是设计了这个接口需要哪些方法。

//加载文本的客户资料
public abstract List<String> loadCustomerFormTxt(String path);
//填充属性到客户对象
//public 和 abstract可以省略
Customer fillCustomer(List<String> props);
/**
 * 保存客户对象到Excel
 * @param cst 客户对象
 * @param excelPath Excel的路径
 * @return
 */
boolean saveToExcel(Customer cst,String excelPath);

我是接口管理员,是设计人员,我觉得这样设计是合理的,可以完成当前的项目需求。那就ok了,写程序就是这样,得先有图纸,然后图纸要评审,评审完成后再交给施工人员去施工。


在这个项目里面,我就是那个设计者,新同事小A是技术落地。


那什么时候需要用到多态呢?我想,应该就是有多人协作,项目比较大的时候,必须要先做好设计!然后,开发人员再根据项目经理写好的接口去做技术实现。


“说的不错。”这时候,老板走了过来。


“小白啊,你能悟到这一点,很是难得。没错,多态最核心的意义就是如此,接口的意义就是为了规范开发的代码。”


“那真正的企业里面做项目也有接口管理员吗,是不是也要像这样先做设计?”我激动地问道。


只见老板的脸轻微地抽搐了一下,仿佛想起来各种被烂代码击垮的往事,“这个,小白啊,虽然这样做是最好的。但是很多企业里面,都是以效率优先,也不管到底合不合理,也没有需求和代码评审。都是一个项目分给你,干就完了。”


“…”


一个对象,怎么去遍历呢?一种做法是实现Iterator接口,为啥,请容我慢慢道来。

首先我们思考一个问题,如果要去遍历一个对象,我们需要知道什么?


答案很显而易见,就是我们需要知道,当我们从这个对象中随便拿出一个元素的时候,还有没有下一个元素?如果有,下一个元素是什么?


如果我们能把这两个问题给解决了,那就可以去遍历了。

7abd7368d75a22ffd9c3f75f1bd7e59c.png


所以,我们需要在class中添加两个方法,分别为:

public boolean hasNext();
public Object next();


这时候又要看到接口的魅力了,瞧瞧,我们自己都能想到的问题,你说Java的创建者能想不到吗?所以,在 java.util 包里面,就存在一个Iterator接口。这个接口的意思就是“可遍历的”。


如果我们实现了这个接口,就必须要实现里面的hasNext方法和next方法。如果翻开Iterator的源码,我们还会发现有几个方法竟然有方法体。我们重点看这个方法:

default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }

这是JDK1.8以后才有的,接口允许写方法体了。我们如果实现了这个接口,也就默认拥有了这个方法。

这个方法干啥用的,待会再说,我们先去实现hasNext方法和next方法。

911faacfe2e04bd50ae9e11602ce24b7.png


首先,添加一个新的属性:

public CustNode nodeForEach; //用于遍历的节点,默认等于第一个节点


遍历的时候,需要专门弄一个属性,避免和currentNode混淆。

然后,编写hasNext方法

@Override
public boolean hasNext() {
    if(nodeForEach == null && firstNode != null) return true;
    return nodeForEach.next != null;
}

思路:第一次调用hasNext方法的时候,nodeForEach肯定是空,如果add成功了,则firstNode肯定不为空,那么就应该返回true。否则,就取nodeForEach.next。那我们在什么时候给nodeForEach赋值呢,自然是next方法。

@Override
public Object next() {
    if(nodeForEach == null && firstNode != null){
        nodeForEach = firstNode;
        return nodeForEach.data;
    }
    nodeForEach = nodeForEach.next;
    return nodeForEach.data;
}

同样做了是否是第一次的判断,如果是第一次进来,那么给nodeForEach赋值为firstNode,然后直接返回第一个元素的data。如果不是第一次进来,那么就取当前循环节点nodeForEach的next,然后别忘了,一定要重新给nodeForEach赋值为next,指针就偏移过去了。最后,再返回nodeForEach的data。


我怕有些人忘记CustNode咋写的了,所以再把代码贴一下。

package tool;
import entity.Customer;
public class CustNode{
  public Customer data; 
  public CustNode next;
  public CustNode(Customer data){
    this.data = data;
  }
}
default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }

解读一下这段源码,<? super E>是泛型,现在你简单理解为是某个类型E的父类,比如E是Customer,那就是<? super Customer>。有趣的是,如果?就是Customer,也是符合规则的。所以,这是一种** >= **的关系。


Consumer<? super E> action,Consumer也是一个接口,意思是消费者。它的源码如下:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

看不懂没关系,现在我们只需要知道它里面有一个很重要的方法是accept:

void accept(T t);

T代表某一个对象,为啥我们要关注这个方法,因为forEachRemaining里面用到了。


default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }


Objects.requireNonNull(action);表示判断这个Consumer是否是空,如果为空的话肯定就报错了。


然后是一个while循环,当hasNext方法返回true的时候,就执行action.accept(next());


next() 不就是我们链表的下一个元素嘛,所以整个方法的意思就是,你需要一个Consumer的实现类,比如叫做ConsumerImpl,然后实现accept方法。接着创建ConsumerImpl类的对象,作为参数传入forEachRemaining方法,就可以实现遍历了。


在这个流程中,思考一下,我们最需要关注的是啥,是不是这个accept方法,这个方法就是让我们去设计遍历的时候,对每一个元素做些什么?

public Customer(String name){
    this.setName(name);
}

61a12abe4722a06c8ebe44b447543887.png


可惜了,Java不支持直接传入一个方法, 在Java的语法里面,是不能直接传入一个方法的,在下面的代码中

default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }

我们希望直接传入一个Consumer的实现类对象,里面包含了accept方法,至于这个实现类叫什么名字,我们不关心。也许叫**ConsumerImpl1,****ConsumerImpl2。**随便叫什么,我的关注的点其实是在accept方法。


还好,Java提供了一种方案来帮我们偷这个懒,叫做**“new一个匿名实现类”,**也有的地方叫做匿名内部类。

@Override
    public boolean saveToExcel(TuziLinkedList customers, String excelPath) {
        customers.forEachRemaining(new Consumer() {
            @Override
            public void accept(Object o) {
                //把Object类型的o对象,强制转化成Customer类型
                Customer cst = (Customer) o;
                System.out.println(cst.getName() + "已经成功保存到Excel!");
            }
        });
        return true;
    }

从表面来看,就是new了一个接口。虽然我们说接口是不能new的,必须要写一个类去实现这个接口,然后去new实现类。但是,从语义上看这个写法,直接理解成new一个接口是非常贴切的。

在CustomerServiceImpl直接写一个main方法来测试。

public static void main(String[] args) {
        TuziLinkedList customers = new TuziLinkedList();
        customers.add(new Customer("孙悟空"));
        customers.add(new Customer("猪八戒"));
        customers.add(new Customer("沙和尚"));
        customers.add(new Customer("唐僧"));
        new CustomerServiceImpl().saveToExcel(customers,null);
    }

效果:

孙悟空已经成功保存到Excel! 
猪八戒已经成功保存到Excel! 
沙和尚已经成功保存到Excel! 
唐僧已经成功保存到Excel!


hasNext方法有一个bug,就是如果我没有给customers对象调用add方法,直接放进saveToExcel方法,会报错。

你能修改代码,解决这个问题吗?

29dfb27fce93607552b9ea1b33edf9a7.png

因为如果没有数据,nodeForEach只会是null,我们的代码逻辑,需要保证第一次hasNext的时候返回true,才玩得转。因为只有第一次hasNext的时候返回true,才会去调用next方法,同时会给nodeForEach赋值,下一次hasNext的时候,nodeForEach就有值了。所以,修改bug的方法,就是加一个非空判断。


相关文章
|
5天前
|
Web App开发 JavaScript 前端开发
《手把手教你》系列技巧篇(三十九)-java+ selenium自动化测试-JavaScript的调用执行-上篇(详解教程)
【5月更文挑战第3天】本文介绍了如何在Web自动化测试中使用JavaScript执行器(JavascriptExecutor)来完成Selenium API无法处理的任务。首先,需要将WebDriver转换为JavascriptExecutor对象,然后通过executeScript方法执行JavaScript代码。示例用法包括设置JS代码字符串并调用executeScript。文章提供了两个实战场景:一是当时间插件限制输入时,用JS去除元素的readonly属性;二是处理需滚动才能显示的元素,利用JS滚动页面。还给出了一个滚动到底部的代码示例,并提供了详细步骤和解释。
28 10
|
12天前
|
Java 测试技术 Python
《手把手教你》系列技巧篇(三十六)-java+ selenium自动化测试-单选和多选按钮操作-番外篇(详解教程)
【4月更文挑战第28天】本文简要介绍了自动化测试的实战应用,通过一个在线问卷调查(&lt;https://www.sojump.com/m/2792226.aspx/&gt;)为例,展示了如何遍历并点击问卷中的选项。测试思路包括找到单选和多选按钮的共性以定位元素,然后使用for循环进行点击操作。代码设计方面,提供了Java+Selenium的示例代码,通过WebDriver实现自动答题。运行代码后,可以看到控制台输出和浏览器的相应动作。文章最后做了简单的小结,强调了本次实践是对之前单选多选操作的巩固。
24 0
|
1天前
|
安全 Java 调度
Java一分钟:多线程编程初步:Thread类与Runnable接口
【5月更文挑战第11天】本文介绍了Java中创建线程的两种方式:继承Thread类和实现Runnable接口,并讨论了多线程编程中的常见问题,如资源浪费、线程安全、死锁和优先级问题,提出了解决策略。示例展示了线程通信的生产者-消费者模型,强调理解和掌握线程操作对编写高效并发程序的重要性。
32 3
|
1天前
|
Java API
Java 接口
5月更文挑战第6天
|
2天前
|
前端开发 Java 测试技术
《手把手教你》系列技巧篇(四十二)-java+ selenium自动化测试 - 处理iframe -下篇(详解教程)
【5月更文挑战第6天】本文介绍了如何使用Selenium处理含有iframe的网页。作者首先解释了iframe是什么,即HTML中的一个框架,用于在一个页面中嵌入另一个页面。接着,通过一个实战例子展示了在QQ邮箱登录页面中,由于输入框存在于iframe内,导致直接定位元素失败。作者提供了三种方法来处理这种情况:1)通过id或name属性切换到iframe;2)使用webElement对象切换;3)通过索引切换。最后,给出了相应的Java代码示例,并提醒读者根据iframe的实际情况选择合适的方法进行切换和元素定位。
7 0
|
2天前
|
存储 安全 Java
Java一分钟之-Map接口与HashMap详解
【5月更文挑战第10天】Java集合框架中的`Map`接口用于存储唯一键值对,而`HashMap`是其快速实现,基于哈希表支持高效查找、添加和删除。本文介绍了`Map`的核心方法,如`put`、`get`和`remove`,以及`HashMap`的特性:快速访问、无序和非线程安全。讨论了键的唯一性、`equals()`和`hashCode()`的正确实现以及线程安全问题。通过示例展示了基本操作和自定义键的使用,强调理解这些概念对编写健壮代码的重要性。
6 0
|
2天前
|
存储 安全 Java
Java一分钟之-集合框架进阶:Set接口与HashSet
【5月更文挑战第10天】本文介绍了Java集合框架中的`Set`接口和`HashSet`类。`Set`接口继承自`Collection`,特征是不允许重复元素,顺序不确定。`HashSet`是`Set`的实现,基于哈希表,提供快速添加、删除和查找操作,但无序且非线程安全。文章讨论了`HashSet`的特性、常见问题(如元素比较规则、非唯一性和线程安全性)以及如何避免这些问题,并提供了代码示例展示基本操作和自定义对象的使用。理解这些概念和注意事项能提升代码效率和可维护性。
9 0
|
2天前
|
存储 安全 算法
Java一分钟之-Java集合框架入门:List接口与ArrayList
【5月更文挑战第10天】本文介绍了Java集合框架中的`List`接口和`ArrayList`实现类。`List`是有序集合,支持元素重复并能按索引访问。核心方法包括添加、删除、获取和设置元素。`ArrayList`基于动态数组,提供高效随机访问和自动扩容,但非线程安全。文章讨论了三个常见问题:索引越界、遍历时修改集合和并发修改,并给出避免策略。通过示例代码展示了基本操作和安全遍历删除。理解并正确使用`List`和`ArrayList`能提升程序效率和稳定性。
7 0
|
2天前
|
Java
【JAVA进阶篇教学】第四篇:JDK8中函数式接口
【JAVA进阶篇教学】第四篇:JDK8中函数式接口
|
3天前
|
前端开发 测试技术 Python
《手把手教你》系列技巧篇(四十一)-java+ selenium自动化测试 - 处理iframe -上篇(详解教程)
【5月更文挑战第5天】本文介绍了HTML中的`iframe`标签,它用于在网页中嵌套其他网页。`iframe`常用于加载外部内容或网站的某个部分,以实现页面美观。文章还讲述了使用Selenium自动化测试时如何处理`iframe`,通过`switchTo().frame()`方法进入`iframe`,完成相应操作,然后使用`switchTo().defaultContent()`返回主窗口。此外,文章提供了一个包含`iframe`的HTML代码示例,并给出了一个简单的自动化测试代码实战,演示了如何在`iframe`中输入文本。
14 3