Как да открием двусмислени извиквания на методи, които биха причинили ClassCastException в Java 8?

В момента сме в процес на мигриране на приложение от Java 7 към Java 8. След като коригирах някои проблеми с компилацията, се натъкнах на проблем, подобен на следния въпрос: Грешка на ClassCast: Java 7 срещу Java 8.

За да обобщим, ето примерен код, който показва проблема:

public class Test {
    public static void main(String[] args) {
        System.out.println(String.valueOf(getVal("xxx"))); // 7: prints the result, 8: Exception 
    }

    @SuppressWarnings("unchecked")
    public static <T> T getVal(String param) {
        // do some computation based on param...
        return (T) result; // actual return type only depends on param so the caller knows what to expect
    }
}

Идеята беше, че ще настояваме обаждащият се да знае очаквания тип и това ще му избегне изрично прехвърляне (Не казвам, че това е добра идея...). В много случаи повикващият просто очаква Object, така че изобщо не е имало имплицитно предаване.

Както беше посочено във въпроса по-горе, примерът String.valueOf работи добре в Java 7, тъй като нямаше извод за тип, следователно беше прието Object. Сега в Java 8 компилаторът избира най-специфичния тип (тук char[]), който причинява ClastCastException по време на изпълнение.

Проблемът е, че имаме около 350 извиквания на този getVal метод. Има ли начин за откриване на претоварени извиквания на методи, които биха се различавали между Java 7 и Java 8? Т.е. открива кога компилаторът на Java 8 ще избере различен метод от компилатора на Java 7.


person Didier L    schedule 28.05.2015    source източник
comment
Имахме подобен метод, който също се извикваше на стотици места. Замених го, като въведох допълнителен параметър Class<T> clazz. По този начин можете да имате изрична обработка на грешки: или върнете null, или хвърлете някакво по-специфично изключение. Току-що направих това ръчно, изразходвайки около половин час. Съветвам ви да направите същото. Не знам как да го автоматизирам, така че това не се квалифицира като отговор на вашия въпрос.   -  person Tagir Valeev    schedule 29.05.2015
comment
Има прост отговор: не потискайте „непроверените“ предупреждения. Вече ви казват, че кодът ви е грешен. Ако искате да поправите кода си сега, когато е твърде късно, просто потърсете срещания на @SuppressWarnings("unchecked")...   -  person Holger    schedule 29.05.2015
comment
@Holger Бихте ли могли да кажете това на моя колега, когато той внедри това през 2011 г.? ;-) Вече знаех, че е лоша идея на първо място (макар и не толкова лоша, колкото е сега в Java 8). Освен това няма @SuppressWarnings в самото извикване на метода, има го само в декларацията на getVal. Премахването на тази анотация тук няма да покаже нито една от проблемните употреби.   -  person Didier L    schedule 01.06.2015
comment
Е, начинът, по който getVal е деклариран, е проблемът. Така че търсенето на анотацията ще ви отведе на правилното място. След това коригирането на подписа, разбира се, ще доведе до грешки на компилатора при повикващите, така че те ще бъдат лесни за намиране. Освен ако не използвате IDE, която рефакторира подписа и извикващите наведнъж, което е жизнеспособна алтернатива. Разбира се, след като анотацията ви насочи към проблемния метод, намирането на неговите извикващи работи работи дори без да го променяте, ако използвате прилична IDE. Така че необходимите инструменти са налични...   -  person Holger    schedule 01.06.2015
comment
Мисля, че това наистина ще бъде пътят. Това всъщност не отговаря на въпроса, тъй като ще трябва да променя много повече код от няколкото проблемни обаждания, но поне ще реши проблема ни. Освен това изглежда, че промяната на типа връщане на Object автоматично ще коригира проблемните обаждания, така че по ирония на съдбата няма да има промяна там (и може би никога няма да разбера къде са били :-).   -  person Didier L    schedule 01.06.2015
comment
Трябва да добавя, че дори и да коригирах нашия getVal метод, все още се страхувам, че има някои други методи някъде, които могат да бъдат декларирани по подобен начин (може би не по толкова очевиден начин), което може да причини същия проблем и да бъде трудно забележимо...   -  person Didier L    schedule 04.06.2015


Отговори (2)


По-добра алтернатива би била:

public class Test {
    public static void main(String[] args) {
        System.out.println(String.valueOf(getVal("xxx"))); // 7: prints the result, 8: Exception
    }

    @SuppressWarnings("unchecked")
    public static <T> T getVal(T param) {
        // do some computation based on param...
        return   param; // actual return type only depends on param so the caller knows what to expect
    }
}

който ще работи както в Java 7, така и в Java 8.

person Shirishkumar Bari    schedule 29.05.2015
comment
Това не е алтернатива, тъй като вие налагате, че връщаният тип е същият като параметъра (който в моя случай е String и не мога лесно да променя това). Освен това въпросът е да се идентифицират случаите, в които извикванията на претоварени методи са различни между Java 7 и 8, за да се избегне промяната на всички тях или търсенето им на ръка. - person Didier L; 29.05.2015

В крайна сметка решението беше да се промени getVal(), за да се върне Object:

    public static Object getVal(String param) {
        // do some computation based on param...
        return result;
    }

и добавете втори метод, който също приема желания клас като параметър:

    public static <T> T getVal(String param, Class<T> clazz) {
        return clazz.cast(getVal(param));
    }

след това поправете всички проблеми с компилацията (където повикващият не е очаквал Object) чрез добавяне на съответния клас параметър.

Добавянето на кастинг също щеше да свърши работа, но щеше да предизвика много предупреждения за безусловни кастинги. Безусловното предаване всъщност все още е там (чрез параметъра clazz), но това позволява лесно да се идентифицират всички викащи, които се нуждаят от предаване, тъй като използват метода с 2 параметъра.

Освен това – и това е много специфично за този случай – изглежда, че самият param често е резултат от извикване на метод на някои TypedParam<T>, където T е очакваният тип връщане на getVal() и който също съдържа класа на T.

По този начин бих могъл да приложа допълнителен удобен метод:

    public static <T> T getVal(TypedParam<T> param) {
        return getVal(param.stringValue(), param.getValueClass());
    }

и замени всички getVal(param.stringValue()) само с getVal(param).

Това решение не разрешава общия случайоткриване на претоварени извиквания на методи, които биха се различавали между Java 7 и Java 8 – но го разрешава за методи, за които е известно, че причиняват този проблем . И оттогава не го намерихме другаде.

person Didier L    schedule 11.01.2016