I had a program crash because of bad data stored in a database recently. This confused me, because I thought I had a catch to prevent this.
The intent of the following code is to compare employee badge numbers and sort them. If there's an error, return -1 and soldier on -- don't stop because one of several thousand badge numbers is wrong:
public int compare(Employee t, Employee t1) { Integer returnValue = -1; try { Integer tb = Integer.parseInt(t.getBadgeNumber()); Integer t1b = Integer.parseInt(t1.getBadgeNumber()); returnValue = tb.compareTo(t1b); } catch (Exception e) { returnValue = -1;//useless statement, I know. } return returnValue; } When the bad badge number hit (as t in this case), I got an "java.lang.IllegalArgumentException: Comparison method violates its general contract!" error instead of returning the -1 in the catch.
What don't I understand about the catch here?
The full stacktrace:
16-May-2018 14:28:53.496 SEVERE [http-nio-8084-exec-601] org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() for servlet [RequestServlet] in context with path [/AppearanceRequest] threw exception java.lang.IllegalArgumentException: Comparison method violates its general contract! at java.util.TimSort.mergeHi(TimSort.java:868) at java.util.TimSort.mergeAt(TimSort.java:485) at java.util.TimSort.mergeForceCollapse(TimSort.java:426) at java.util.TimSort.sort(TimSort.java:223) at java.util.TimSort.sort(TimSort.java:173) at java.util.Arrays.sort(Arrays.java:659) at java.util.Collections.sort(Collections.java:217) at org.bcso.com.appearancerequest.html.NotifierHTML.getHTML(NotifierHTML.java:363) at org.bcso.com.appearancerequest.AppearanceRequestServlet.processRequest(AppearanceRequestServlet.java:96) at org.bcso.com.appearancerequest.AppearanceRequestServlet.doGet(AppearanceRequestServlet.java:565) at javax.servlet.http.HttpServlet.service(HttpServlet.java:618) at javax.servlet.http.HttpServlet.service(HttpServlet.java:725) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:301) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) at org.netbeans.modules.web.monitor.server.MonitorFilter.doFilter(MonitorFilter.java:393) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:219) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:503) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:136) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:74) at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:610) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:516) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1015) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:652) at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.process(Http11NioProtocol.java:222) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1575) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1533) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:745) The calling code:
List<Employee> employeeList = DatabaseUtil.getEmployees(); Collections.sort(employeeList, new BadgeComparator());
With a try catch, you can handle an exception that may include logging, retrying failing code, or gracefully terminating the application. Without a try catch, you run the risk of encountering unhandled exceptions. Try catch statements aren't free in that they come with performance overhead.
Does try { } catch make program slower? No. There's a performance hit to actually throwing an exception, but if you don't catch it, it'll just propagate up and potentially be unhandled.
It is possible to write code for almost of your business processes without ever using a single try-catch. They should be the exception, not the rule. If you're not sure if you can get an exception, experiment.
Exactly. Putting everything in a try/catch statement is usually a sign of an inexperienced developer who doesn't know much about exceptions IME.
The exception (whatever it was) was caught by catch (Exception e). You didn't log this exception, so you don't know what it was. You should log it somehow so you know what really happened.
The problem occurs when you return -1. This allows for the possibility of inconsistent ordering, which Java's current sorting algorithm sometimes catches. In short, returning -1 on an error means that you are asserting that both a < b and b < a are true, because the exception will be caught in both cases. This is logically incorrect. The sorting algorithm detects this and throws the IllegalArgumentException. Note that the compare method is not in your stack trace; it's the call to Collections.sort.
In addition to logging the exception, handle it before you even get to the comparison step in your program. If you have to parse the string as an integer, do that when creating the Employee objects, so that the validation occurs before you even get to the sorting step in your program. A Comparator shouldn't have to validate data; it should only compare the data.
java.lang.IllegalArgumentException: Comparison method violates its general contract!
The exception is not thrown from within your try. That is why it is not caught. The exception comes from NotifierHTML.java:363 in your code where you call Collection#sort which uses a TimSort class. The exception is then thrown from TimSort.java:868 by the TimSort#mergeHi method.
It tells you that your implementation of the Comparator#compare method is wrong. It violates the contract, as explained in its documentation:
Compares its two arguments for order. Returns a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.
The implementor must ensure
sgn(x.compareTo(y)) == -sgn(y.compareTo(x))for allxandy. (This implies thatx.compareTo(y)must throw an exception iffy.compareTo(x)throws an exception.)The implementor must also ensure that the relation is transitive:
(x.compareTo(y) > 0 && y.compareTo(z) > 0)impliesx.compareTo(z) > 0.Finally, the implementor must ensure that
x.compareTo(y) == 0implies thatsgn(x.compareTo(z)) == sgn(y.compareTo(z)), for allz.
Your implementation violates one of those requirements and the method detected that.
The problem is that you return -1 if an error occurs. Suppose you have two values first and second. And that at least one of them will provoke the exception.
So if you want to compare first with second, you get -1:
compare(first, second) -> -1 Which means that first is smaller than second. But if you compare it the other way you get -1 too:
compare(second, first) -> -1 Because the exception is thrown in both variants, which leads to your return -1;. But this means your compare method says:
first < second second < first Both at the same time, which is logically incorrect and violates the contract.
You need to correctly define where in your ordering unparsable content is placed at. For example let us define that it is always smaller than any number. So we want
text < number What do we do if both are unparsable? We could say they are equal, we could compare them lexicographical. Let's keep it simple and say that any two texts are considered equal:
text = text We implement this by checking which of the arguments are unparseable and then returning the correct value:
@Override public int compare(Employee first, Employee second) { Integer firstValue; Integer secondValue; try { firstValue = Integer.parseInt(first.getBadgeNumber()); } catch (NumberFormatException e) { // Could not parse, set null as indicator firstValue = null; } try { secondValue = Integer.parseInt(second.getBadgeNumber()); } catch (NumberFormatException e) { // Could not parse, set null as indicator secondValue = null; } if (firstValue == null && secondValue != null) { // text < number return -1; } if (firstValue != null && secondValue == null) { // number > text return 1; } if (firstValue == null && secondValue == null) { // text = text return 0; } // Both are numbers return Integer.compare(firstValue, secondValue); } As hinted in the comments you could replace your whole custom Comparator class by the following statement which generates the same Comparator:
Comparator<Employee> comp = Comparator.nullsLast( Comparator.comparing(e -> tryParseInteger(e.getBadgeNumber()))); Together with a tryParseInteger method like this:
public static Integer tryParseInteger(String text) { try { return Integer.parseInt(text); } catch (NumberFormatException e) { return null; } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With