Java速通 Day4 – 类和泛型

/ 0评 / 0

前面说到了我们自己做得轮子,就是普通类,以及枚举类,但是在Java中还有很多类,比如我们知道类是可以继承的,但是无法多继承,如果这个类要实现类似有多个父类的感觉怎么办,这时候就要用到抽象类,抽象类就是只提供方法的声明但是并不实现他.

抽象类只提供抽象方法,无法实例化,必须先被继承实现后才能实例化.因为抽象类等于未完工状态,另外只有抽象方向,没有抽象属性,先新建一个鸡的抽象类.

package com.hello;

abstract public class Chicken {
    protected String name;

    // 抽象的,需要等着被实现!
    abstract public void call();
}

然后继承出公鸡和母鸡,我这里只写一个类,另一个类似.

package com.hello;

public class Cock extends Chicken{
    public Cock(){
        this.name = "公鸡";
    }

    @Override
    public void call() {
        System.out.println("公鸡在叫!");
    }
}

然后测试一下.

但是既然抽象类要继承,这也还是解决不了想多实现几个东西的目标啊,所以我们要用接口类,先写了一个动物的抽象类,他们都有名字.

package com.hello;

public class Animal {
    protected String name;
}

再写一个会叫的接口,因为接口里所有方法都抽象,就不用特别说明抽象了.

package com.hello;

public interface Chicken {
    public void call();
}

最后修一修我的公鸡类和母鸡类.

package com.hello;

public class Cock extends Animal implements Chicken {
    public Cock(){
        this.name = "公鸡";
    }

    @Override
    public void call() {
        System.<em>out</em>.println("公鸡在叫!");
    }
}

最后调用结果是一毛一样的,但是我们的公鸡/母鸡类的代码不一样了,interface类可以包含多个方法,如果是由多个的,那都要一一实现,并且一个类可以实现多个interface,比如母鸡可以再继承一个生蛋的interface.

package com.hello;

public class Hen extends Animal implements Chicken,Egg {
    public Hen(){
        this.name = "母鸡";
    }

    @Override
    public void call() {
        System.out.println("母鸡在叫!");
    }

    @Override
    public void egg() {
        System.out.println("生蛋了!");
    }
}

另外接口内部方法不一定得都是抽象方法,他也可以有构造方法.

public interface ExpandBehavior {
	public void run(); // 声明了一个抽象的奔跑方法

	// 默认方法不支持重写,但可以被继承.
	public default String Hello(String name) {
		return "Hello " + name;
	}

	// 接口内部的静态属性也默认为终态属性,所以隐含了final前缀.
	public static int LIVE = 0;
	
	// 静态方法支持重写,但不能被继承.
	public static String Echo(String msg) {
		return msg;
	}

}

因为静态方法不能被继承,所以只能通过ExpandBehavior.Echo来访问了.而Hello方法则可以在实现了这个接口的类中调用(如果作用域允许也可以被实例化对象后的外部调用),但是不可以重写,相当于继承了一个终态方法.

前面都是我们自己做的interface,那有没有什么系统interface呢,当然有的,比如我们数组排序时候用到的,就是用到一个Comparator的接口类,他只有一个方法,比如可以在排序时候提供一些指引.

我先尝试一下常规的排序.

package com.hello;

import java.util.Arrays;
import java.util.Random;

public class basic {
    public static void main(String[] args) {
        Integer[] numbers = new Integer[10];
        Random rnd = new Random();
        for(int i = 0; i < numbers.length; i++) {
            numbers[i] = rnd.nextInt(1000);
        }

        System.out.println(Arrays.toString(numbers));
        Arrays.sort(numbers);
        System.out.println(Arrays.toString(numbers));
    }
}

输出结果我们看到是升序排列,如果我们重写排序中的比较方法会如何.

public class basic {
    public static void main(String[] args) {
        Integer[] numbers = new Integer[10];
        Random rnd = new Random();
        for(int i = 0; i < numbers.length; i++) {
            numbers[i] = rnd.nextInt(1000);
        }

        System.out.println(Arrays.toString(numbers));
        Arrays.sort(numbers, new Comparator<Integer>(
        ) {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1 - o2 > 0 ? -1 : (o1.equals(o2) ? 0:1);
            }
        });
        System.out.println(Arrays.toString(numbers));
    }
}

我们传入了一个interface进去,并实现了这个interface的方法,就变成降序排列了.由于这个只有一个实现方法,所以也可以简写成Lambda,就是省略类声明和方法声明了.

Arrays.sort(numbers, (o1, o2) -> o1 - o2 > 0 ? -1 : (o1.equals(o2) ? 0:1));

是不是很神奇,我们传入的居然是一个类,当然,我们稍微改装下我们的公鸡类,让他也可以传入一个接口,比如这个接口叫吃,就只有一个抽象方法eat,目的是当我传入公鸡饿了就要开始吃东西,但是怎么吃,吃什么需要用户传进去.

最后调用下面代码就可以按照我们想法输出吃饭了.

Cock cock1 = new Cock();
cock1.hungry(()->System.out.println("吃饭了"));

前面已经看到Lambda已经非常简洁了,下面要说的就更加简洁了,在Java中称为方法引用,比如我们想按照字母顺序排序字符串数组,默认的就可以了,但是如果我们要忽略大小写再排序,常规方法我们写一个lambda来比较下,但是这里可以直接使用方法引用也可以完成.

public class basic {
    public static String generateRandomString(int length) {
        StringBuilder sb = new StringBuilder(length);
        ThreadLocalRandom random = ThreadLocalRandom.current();
        for (int i = 0; i < length; i++) {
            boolean uppercase = random.nextBoolean();
            char letter = (char) (random.nextInt(26) + (uppercase ? 'A' : 'a'));
            sb.append(letter);
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        String [] str = new String[10];
        Random rnd = new Random();
        for(int i =0;i < str.length;i++){
            str[i] = generateRandomString(rnd.nextInt(10) + 1);
        }
        
        System.out.println(Arrays.toString(str));
        Arrays.sort(str,String::compareToIgnoreCase);
        System.out.println(Arrays.toString(str));
    }
}

Java会理解传入的String::compareToIgnoreCase是什么意思,就不用我们再展开了,比如下面这一行,我不用展开进去,我就知道他是一个叫Arithmetic的类的calculate方法,第一个参数传入一个接口,这里刚好可以套用Math.max,后面是两个参数,会计算3和2这两个数字中谁更大.

Arithmetic.calculate(Math::max, 3, 2); 

即使不是静态方法,比如某个方法需要实例化,实例化后依然是可以使用[实例名::方法名]使用,比如下面这个举例,我刚好在MathUtil中的方法可以实现Calculator接口的方法,那么就算他需要实例化,我们也可以实例化后使用方法引用.

public class MathUtil {
	public double add(double x, double y) {
		return x + y;
	}
}

public interface Calculator {
	public double operate(double x, double y);
}

public class Arithmetic {
	public static double calculate(Calculator calculator, double x, double y) {
		return calculator.operate(x, y);
	}
}

public class basic {
    public static void main(String[] args) {
        MathUtil math = new MathUtil();
        double result;
		result = Arithmetic.calculate(math::add, 3, 2);
		System.out.println(result);
    }
}

所有程序都不一定好用,特别是程序越来越复杂,很可能产生各种异常,遇到异常就要处理,这样才能增强程序的健壮性.比如下面这么一句话就能制造除不尽的异常,因为BigDecimal储存的数据必须准确.

BigDecimal result = BigDecimal.valueOf(100).divide(BigDecimal.valueOf(3));

执行后就报告异常了.

Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
	at java.base/java.math.BigDecimal.divide(BigDecimal.java:1804)
	at com.hello.basic.main(basic.java:7)

类似异常还有非常多,这个异常是由divide抛出的,既然是抛出的,我们就可以捕捉,当我们改用try...catch捕获异常后,发生异常时代码仍会继续执行.

public class basic {
    public static void main(String[] args) {
        BigDecimal result;

        for(int i = 0;i < 10;i++) {
            try{
                result = BigDecimal.valueOf(100).divide(BigDecimal.valueOf(i));
                System.out.println(result);
            }catch(ArithmeticException e){
                System.out.println("当前期望分母为" + i + ",异常原因:" + e.getMessage());
            }
        }
    }
}

当发生异常时执行异常代码,且继续执行当前程序.

异常捕获可以同时捕捉多个错误,所有异常都继承于Exception,如果不管是否有异常都需要执行,则需要finally包裹.

这么我们就知道捕获异常,现在我们来制作一些自己的异常,前面说了所有异常都继承于Exception,所以我们新建一个类也继承Exception.

public class ArrayIsNullException extends Exception{
    private static final long serialVersionUID = 1L;
    
    public ArrayIsNullException(String message){
        super(message);
    }
}

同样方法创建ArrayIsNullException和ArrayOutOfIndexException,然后创建一个getItem的方法,这个方法就是提取数组里的元素.

首先在方法的开头添加throws注明这个函数可以丢出哪些异常,然后在需要的地方使用throw new 异常类名(异常消息)进行抛出异常.

public class basic {
    public static int getItem(int[] array,int index) throws ArrayIsNullException,ArrayOutOfIndexException{
        if(index<0 || index>=array.length){
            throw new ArrayOutOfIndexException("Index out of bounds");
        }else if(array == null){
            throw new ArrayIsNullException("Array is null");
        }else{
            return array[index];
        }
    }

    public static void main(String[] args) {
        int[] int1 = new int[]{1,2,3,4,5,6,7,8,9};

        try{
            getItem(int1,20);
        }catch (Exception e){
            e.printStackTrace();
        }
        
    }
}

当然,我们这个数组就没20这个位置的元素,所以会抛出异常.

com.world.hello.ArrayOutOfIndexException: Index out of bounds
	at com.world.hello.basic.getItem(basic.java:8)
	at com.world.hello.basic.main(basic.java:20)

异常是容易发生,但是我们不希望他发生,比如刚才的数无法精准表达的错误,可以先行修改为保留一定量的小数,比如改成这样.

BigDecimal.valueOf(100).divide(BigDecimal.valueOf(i), 64, BigDecimal.ROUND_HALF_UP);

对于上面的数组越界问题,可以在执行这个函数前先行判断要传递的参数,实在无法避免,再使用异常捕获,但是对于哪些非常难搞的空指针错误,初期避免是有一定的麻烦的,所以Java中还有一个Optional可选器,在讲可选器之前要先简单搞明白容器的知识.

常见的集合有HashSet(哈希),TreeSet(二叉树),HashMap(哈希图),TreeMap(红黑树),ArrayList(列表),LinkedList(链表),其中容器的意思就是Set(集合),Map(映射),List(清单).

比如我创建的一个HashSet,添加了10个元素,然后把他遍历打印出来.

HashSet<Integer> set = new HashSet<>();
for(int i = 0;i < 10;i++) {
    set.add(i);
}

System.out.println("集合容量:" + set.size());
set.forEach(System.out::println);

输出大概是这样的.

集合容量:10
0
1
2
3
4
5
6
7
8
9

当然Set也可以用传统数组的方法遍历,或者使用迭代器遍历也是可以的.

Iterator<Integer> iterator = set.iterator();
while(iterator.hasNext()) {
    System.out.println(iterator.next());
}

既然有数组了,为什么还要用Set,那当然就是因为Set有更好的地方,Set有一下方法.

如何集合单纯是储存普通变量无疑限制适用范围,我们现在继续以Fruit类来创建一个HashSet,比如下面代码,他应该打印成什么呢.

    public static void main(String[] args) {
        HashSet<Fruit> fruits = new HashSet<>();
        fruits.add(new Fruit("苹果",70));
        fruits.add(new Fruit("苹果",50));
        fruits.add(new Fruit("雪梨",30));
        fruits.add(new Fruit("菠萝",55));
        fruits.forEach(System.out::println);
    }

很可惜,虽然我们苹果是先放进去的,但是打印结果似乎不总是如愿.直接换成TreeSet可以确保打印顺序吗?当然可以,但是还需要对类进行处理,添加两个方法.

public class Fruit implements Comparable<Fruit>{
    // 其他省略

    @Override
    public int hashCode() {
        return this.name.hashCode() + Integer.hashCode(this.weight);
    }

    @Override
    public boolean equals(Object obj) {
       if(obj instanceof Fruit f){
           return Objects.equals(f.getName(), this.name) && f.getWeight() == this.weight;
       }
       return false;
    }

    @Override
    public int compareTo(Fruit o) {
        return o.getWeight() > this.weight?1:(Objects.equals(o.getName(), this.name) ?0:-1);
    }
}

这样TreeSet就知道如何比对了.

Set只能储存单一东西,如果我要储存一个KV关系,那就是要用Map,也一样分为HashMap和TreeMap,比如我要分别确定每个水果是谁的.

TreeMap<String,Fruit> map = new TreeMap<>();
map.put("小明",new Fruit("苹果",200));
map.put("小李",new Fruit("雪梨",500));
map.put("小芳",new Fruit("菠萝",100));
map.forEach((k,v)->{System.<em>out</em>.println(k + "拥有的水果是" + v);});

当然同样也可以使用迭代器,支持的方法和Set基本是一样,除了添加元素是put,其他没什么差别.

最后一个容器是List,分为LinkedList和ArrayList,其中ArrayList时间复杂第是O(1),支持从头尾增删(addFirst,addLast,removeFirst,removeLast,getFirst,getLast),也支持随机访问,而LinkedList不支持随机访问,获取元素需要遍历链表,但是在数量不固定,变化较多情况下,LinkedList更合适.

那么容器和数组既然有类似的地方,他们是不是就可以互相转换,当然,除了直接传入数组,还可以传入多个参数,直接构成对应的容器.

Arrays.asList("苹果","香蕉","西瓜");
List.of("苹果","香蕉","西瓜");
Set.of("苹果","香蕉","西瓜");
Map.of("苹果","小明","香蕉","小李","西瓜","小芳");

创建到的Map转HashMap,其他也是同理.

HashMap map2 = new HashMap(map);

容器还可以使用流式处理来简化代码.只需要在容器后调用stream方法,就可以在后续继续处理.

比如这样遍历小于30重量的所有Fruit对象.

list.stream().filter(fruit -> fruit.getWeight() < 30).forEach(System.<em>out</em>::println);

流式处理常见加工指令.

前面的容器增强了数组不方便储存一批对象,或者KV关系的,或者链表关系的问题,那么泛型就是解决一些方法的重复工作量问题.

比如这个简单的比较大小方法,他很好用,但是,他却只能用于Interger类型.

public class basic {
    public static Integer max(Integer a,Integer b) {
        return a > b ? a : b;
    }

    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        System.<em>out</em>.println(<em>max</em>(a,b));
    }
}

有没有办法让他能用在更宽的地方呢,用Number这个类作为参数看看?看起来同时支持了多种类型了.

public class basic {
    public static Number max(Number a,Number b) {
        return a.doubleValue() > b.doubleValue() ? a : b;
    }

    public static void main(String[] args) {
        BigDecimal a = BigDecimal.<em>valueOf</em>(1);
        BigDecimal b = BigDecimal.<em>valueOf</em>(2);

        BigInteger c = BigInteger.<em>valueOf</em>(3);
        BigInteger d = BigInteger.<em>valueOf</em>(4);

        Integer e = 5;
        Integer f = 6;

        System.<em>out</em>.println(<em>max</em>(a,b));
        System.<em>out</em>.println(<em>max</em>(c,d));
        System.<em>out</em>.println(<em>max</em>(e,f));
    }
}

这个max方法意思是,传入的只要是Number或者是Number下的所有类型的,都可以,那么改成泛型格式写法就是这样.

public static <T extends Number> T max(T a,T b) {
    return a.doubleValue() > b.doubleValue() ? a : b;
}

T扩展为Number类型,返回T类型,传入类型是T,只要传入的符合T类型,进去后就是实际传入的类型.

如果我希望有一个方法,能传入任何东西,那么他应该继承Object,而这是特殊的.

public class basic {
    public static <T> String printObj(T[] ts){
        StringBuilder sb = new StringBuilder();
        for(T t:ts) {
            sb.append(t.toString());
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        BigDecimal a = BigDecimal.<em>valueOf</em>(1);
        BigDecimal b = BigDecimal.<em>valueOf</em>(2);

        BigInteger c = BigInteger.<em>valueOf</em>(3);
        BigInteger d = BigInteger.<em>valueOf</em>(4);

        Integer e = 5;
        Integer f = 6;
        
        Object[] o = {a,b,c,d,e,f};
        
        System.<em>out</em>.println(<em>printObj</em>(o));
    }
}

现在已经理解泛型他是某一种宽泛的描述,那么他是不是也可以用在类里,我们做一个SimpleList的类.

public class SimpleList <T> {
    private List<T> list;

    public SimpleList(List<T> list) {
        this.list = list;
    }

    public void printHashCode() {
        for (T t : list) {
            System.<em>out</em>.println("元素HashCode = " + t.hashCode());
        }
    }

}

简单测试一下.

public class basic {
    public static void main(String[] args) {
        ArrayList<Fruit> list = new ArrayList<>();
        list.add(new Fruit("apple", 10));
        list.add(new Fruit("orange", 20));
        list.add(new Fruit("pear", 30));
        list.add(new Fruit("grape", 40));

        SimpleList simpleList = new SimpleList(list);
        simpleList.printHashCode();
    }
}

在Java中内置了几个泛型接口,比如之前用的Compartor就是泛型接口,所以他可以传递各种参数.

public interface Comparator<T> {
    int compare(T a, T b);
}

断言接口,传入一个对象,返回真或者假,并不修改数据本身.

如果需要修改数据本身,需要使用Consumer接口,当然,accept如果传入的是String是不能修改元素的.

刚才的Consumer接口虽然可以修改,但是等于破坏原数据,并且传入什么类型,传出就得什么类型,所以这里还有一个Function接口.

饶了这么一大个圈,可以回来继续讲如何用Optional这个可选器来避免一些问题了.我们把水果类稍微修改一下.

package com.world.hello;

public class Fruit{
    private String name; // 名称
    private String color; // 颜色
    private Double weight; // 重量
    private Double price; // 价格

    public Fruit(String name, String color, Double weight, Double price) {
        this.name = name;
        this.color = color;
        this.weight = weight;
        this.price = price;
    }

    // 省略了所有set/get方法...

    public String toString() {
        return String.<em>format</em>("品名:%s,颜色:%s,重量:%f,价格:%f)", name,
                color, weight, price);
    }

    public boolean isRed() {
        return this.color.equalsIgnoreCase("red");
    }
}

我们先用正常列表测试一下.

public class basic {
    public static void main(String[] args) {
        ArrayList<Fruit> fruit = new ArrayList<>();
        fruit.add(new Fruit("苹果", "RED", 150d, 10.0));
        fruit.add(new Fruit("柠檬", "green", 250d, 10.0));
        fruit.add(new Fruit("苹果", "red", 300d, 10.0));
        fruit.add(new Fruit("香蕉", "yellow", 200d, 10.0));
        fruit.add(new Fruit("西瓜", "green", 100d, 10.0));
        fruit.add(new Fruit("苹果", "Red", 250d, 10.0));

        List<Fruit> fruitList = fruit.stream().filter(Fruit::isRed).collect(Collectors.toList());
        System.out.println(fruitList);

    }
}

输出结果.

[品名:苹果,颜色:RED,重量:150.000000,价格:10.000000), 品名:苹果,颜色:red,重量:300.000000,价格:10.000000), 品名:苹果,颜色:Red,重量:250.000000,价格:10.000000)]

但是现在程序列表里似乎出了点问题,有一些颜色缺失,也有一些干脆元素都是NULL的.再运行会怎样.

public class basic {
    public static void main(String[] args) {
        ArrayList<Fruit> fruit = new ArrayList<>();
        fruit.add(new Fruit("苹果", "RED", 150d, 10.0));
        fruit.add(new Fruit("柠檬", "green", 250d, 10.0));
        fruit.add(new Fruit("苹果", "red", 300d, 10.0));
        fruit.add(new Fruit("香蕉", "yellow", 200d, 10.0));
        fruit.add(new Fruit("西瓜", "green", 100d, 10.0));
        fruit.add(new Fruit("苹果", null, 250d, 10.0));
        fruit.add(null);

        List<Fruit> fruitList = fruit.stream().filter(Fruit::isRed).collect(Collectors.<em>toList</em>());
        System.<em>out</em>.println(fruitList);
    }
}

毫无疑问NullPointerException,当然我们可以try...catch他,不过我们有更优雅的方法,避免错误.

先用比较简单方法,遍历后看看这个元素是不是NULL,是NULL就不继续.

List<Fruit> fruitList = new ArrayList<>();
for(Fruit f : fruit) {
    if(f != null) {
        if(f.getColor() != null){
            if(f.isRed()){
                fruitList.add(f);
            }
        }
    }
}

System.<em>out</em>.println(fruitList);

这样虽然能解决,但是看起来就有点长了,想一想,如果颜色都是NULL了,那是不是就说明他不能认为是红色,所以我们改一改判断方法.

public boolean isRed() {
    if(this.color != null){
        return this.color.equalsIgnoreCase("red");
    }
    return false;
}

哎,不过是把判断从一个地方转到另一个地方,问题也还是没解决,用可选器修饰一下.

public boolean isRed() {
    return Optional.<em>ofNullable</em>(this.color).map(String::toLowerCase).orElse("not read").equals("red");
}

这样看起来简洁多了,也把剩余没修饰的也改一下.

public class basic {
    public static void main(String[] args) {
        ArrayList<Fruit> fruit = new ArrayList<>();
        fruit.add(new Fruit("苹果", "RED", 150d, 10.0));
        fruit.add(new Fruit("柠檬", "green", 250d, 10.0));
        fruit.add(new Fruit("苹果", "red", 300d, 10.0));
        fruit.add(new Fruit("香蕉", "yellow", 200d, 10.0));
        fruit.add(new Fruit("西瓜", "green", 100d, 10.0));
        fruit.add(new Fruit("苹果", null, 250d, 10.0));
        fruit.add(null);

        List<Fruit> fruitList = new ArrayList<>();

        Optional.ofNullable(fruit).ifPresent(fruits -> {
            fruitList.addAll(fruits.stream().filter(fruit1 -> fruit1 != null).filter(Fruit::isRed).collect(Collectors.toList()));
        });

        System.out.println(fruitList);
    }
}

这样不管数谁是null我都可以避免发生错误,但是这还不是最终,继续简写.

Optional.of(fruit).ifPresent(fruits -> {
fruitList.addAll(fruits.stream().filter(Objects::nonNull).filter(Fruit::isRed).toList());
});

这样是不是看起来清晰多了,代码行数也明显减少.

今天内容也稍微稍微有点多,就此打住了.

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注