简体   繁体   English

我该如何解决泛型实例化问题?

[英]How can I solve generic instantiation problem?

I am making a CSV parser for 3 files (like "city_id";"country_id";"region_id";"name") and I am faced with a generic instantiation problem. 我正在为3个文件(例如“ city_id”;“ country_id”;“ region_id”;“ name”)制作一个CSV解析器,但我遇到了通用的实例化问题。 Is there any way how can I solve it and stick to DRY? 有什么办法可以解决并坚持使用DRY吗? ( I saw that an answer may be to take a T in the constructor but I don`t see how I can use it properly in my situation). (我看到答案可能是在构造函数中使用T,但我看不到如何在我的情况下正确使用它)。

public static <T> List<T> csvParcer(String filePath) {
    List<T> cities = new ArrayList<>();
    String line;
    String[] dividedLine;
    try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
        reader.readLine();
        while ((line = reader.readLine()) != null) {
            dividedLine = line.replace("\"", "").replace(";", " ").split(" ");
            cities.add(new T(dividedLine[0], dividedLine[1], dividedLine[2], dividedLine[3]));
        }
        return cities;
    } catch (IOException ex) {
    }
    return null;
}

Here's the DRYest version I can come up with, using Java 8. 这是我使用Java 8可以想到的DRYest版本。

public static <T> List<T> parseCsvFile(String filePath, Function<String[], T> mapper) {
    return Files.lines(new File(filePath).toPath())
                .map(s -> s.replace("\"", "").split(";"))
                .map(mapper)
                .collect(Collectors.toList());
}

Use like so. 像这样使用。

List<Foo> foos = parseCsvFile("foos.csv", columns -> {
    return new Foo(columns[0], columns[1], columns[2], columns[3]);
});

Why it doesn't work 为什么它不起作用

The thing that doesn't work in Java is the new T(...) instantiation of a generically-given class. 在Java中不起作用的是通用类的new T(...)实例化。 You can only use the new keyword with a specific class name. 您只能将new关键字与特定的类名一起使用。

At run time, your csvParcer() method doesn't even know which class was used for T , for the JVM, T will be replaced by Object . 在运行时,您的csvParcer()方法甚至都不知道T使用了哪个类,对于JVM, T将被Object取代。 So there's no way for your method to know which class to instantiate. 因此,您的方法无法知道要实例化哪个类。 You need to pass something into your method that allows you to instantiate the class you want for that given situation. 您需要向您的方法中传递一些信息,以允许您为给定情况实例化所需的类。

Solution with reflection 反射解决方案

One approach is to add a parameter to your method naming the class you want to instantiate: 一种方法是在方法中添加一个参数,以命名要实例化的类:

public static <T> List<T> csvParcer(String filePath, Class<T> tClazz) {
    List<T> cities = new ArrayList<>();
    String line;
    String[] dividedLine;
    try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
        reader.readLine();
        while ((line = reader.readLine()) != null) {
            dividedLine = line.replace("\"", "").replace(";", " ").split(" ");

            Constructor<T> myConstructor = tClazz.getConstructor(String.class, String.class, String.class);
            T object = myConstructor.newInstance(dividedLine[0], dividedLine[1], dividedLine[2], dividedLine[3]);

            cities.add(object);
        }
        return cities;
    } catch (Exception ex) {
        throw new RuntimeException("Error reading " + filePath, ex);
    }
}

[By the way, I changed the error handling to throw an exception if my method couldn't correctly read and parse the file, as that's the preferred way to tell my caller that he can't get a result.] [顺便说一下,如果我的方法无法正确读取和解析文件,我将错误处理更改为抛出异常,因为这是告诉我的调用者他无法获得结果的首选方法。]

Disadvantage is that you waste runtime performance (not noticable compared to the reading of a CSV text file) and you don't get compile-time errors if the class you need doesn't have a public constructor that accepts exactly four strings. 缺点是浪费运行时性能(与读取CSV文本文件相比并不明显),并且如果您需要的类没有可完全接受四个字符串的公共构造函数,则不会出现编译时错误。

Solution with factory objects 工厂对象的解决方案

That's the approach that Leo already proposed, you pass in an object that encapsulates the instance creation - a "factory" object, and you need one for every different T class that you want to get from your CVS reader. 这就是Leo已经提出的方法,您传入一个封装实例创建的对象-一个“工厂”对象,并且对于要从CVS阅读器获取的每个不同的T类,都需要一个对象。 Leo rewrote your example using the elegant Java-8 streams coding style, but it's also possible in the classic style, closer to your original idea. Leo使用优雅的Java-8流编码样式重写了您的示例,但是在经典样式中也可以实现,更接近您的原始想法。 First we need an interface for the factory: 首先,我们需要一个工厂接口:

public interface TFactory<T> {
    T create(String arg0, String arg1, String arg2, String arg3);
}

The parser method looks like: 解析器方法如下所示:

public static <T> List<T> csvParcer(String filePath, TFactory<T> factory) {
    List<T> cities = new ArrayList<>();
    String line;
    String[] dividedLine;
    try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
        reader.readLine();
        while ((line = reader.readLine()) != null) {
            dividedLine = line.replace("\"", "").replace(";", " ").split(" ");

            T object = factory.create(dividedLine[0], dividedLine[1], dividedLine[2], dividedLine[3]);

            cities.add(object);
        }
        return cities;
    } catch (Exception ex) {
        throw new RuntimeException("Error reading " + filePath, ex);
    }
}

And you use it like this example: 您可以像以下示例一样使用它:

private void example() {
    TFactory<City> cityFactory = new TFactory<City>() {
        @Override
        public City create(String arg0, String arg1, String arg2, String arg3) {
            return new City(arg0, arg1, arg2, arg3);
        }
    };
    List<City> cities = csvParcer("C:\\temp\\cities.csv", cityFactory);
} 

Having four explicit String arguments makes the code more verbose than using a String[] array, but gives you additional compile-time safety. 与使用String []数组相比,具有四个显式的String参数使代码更加冗长,但是为您提供了额外的编译时安全性。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM