Book review: Effective Java

This review covers Effective Java 2nd ed., by Joshua Bloch.  Stoughton, Massachusetts: Addison-Wesley (an imprint of Pearson Education, Inc.), 2008.



From the Foreword:

If you have ever studied a second language yourself outside the classroom, you know that there are three things you must master: how the language is structured (grammar), how to name things you want to talk about (vocabulary), and the customary and effective ways to say everyday things (usage)… This book addresses your third need: customary and effective usage.

And as Bloch says in the Introduction:

This book consists of seventy-eight items, each of which conveys one rule.  The rules capture practices generally held to be beneficial by the best and most experienced programmers.

The items are organized into ten categories:

  • Creating and Destroying Objects
  • Methods Common to All Objects
  • Classes and Interfaces
  • Generics
  • Enums and Annotations
  • Methods
  • General Programming
  • Exceptions
  • Concurrency
  • Serialization

The format of the book is based on Scott Meyers’ Effective C++.

The book is designed as a next-level book for those who already have some experience with the Java programming language rather than as an introduction to the language.

The Good

The book:

  1. Helps the Java non-expert toward usability, robustness, and flexibility.  There are a lot of things to know about Java to use it well.
  2. Gives reasons for his statements. In cases where you may decide to deviate from Bloch’s advice, this gives you the opportunity to examine his reasoning and understand the implications of your decision, so you’re not flying blind.
  3. Makes recommendations concrete by giving examples. For example, in Item 20, the exhortation to prefer class hierarchies to tagged classes is illustrated with a tagged class and its alternative representation as a class hierarchy.  There are helpful code examples like this sprinkled throughout the book.
  4. Includes a succinct summary of the advice at the end of many Items.
  5. Provides expert help in doing common but tricky tasks. For instance, the naive developer may not know the contract that they’re responsible to uphold when overriding the equals() method (Item 8), or may not realize the maintainability implications of marking an object Serializable (Item 74).  Much time and distress can be saved by reading about common pitfalls ahead of time.
  6. Points out how to avoid many surprises. For example, several important details that should be dealt with when making a class Clonable (and the consequences of ignoring them – Item 11); and details about the relationship between arrays and generics (Item 25).
  7. Provides help in doing some advanced tasks well. For example, the chapter on concurrency points out the benefits and details of using the Executor Framework in java.util.concurrent rather than the Thread class (Item 68); the Serialization chapter presents the Serialization Proxy pattern (Item 78);  and Item 17 discusses issues that should be addressed when designing a class for inheritance.
  8. Includes some just plain old great ideas. The book includes several ideas that made me think, “Huh!  Why aren’t we doing this?”  An example is his advice to put methods on your checked exception that help your callers extract the information they want out of it without having to resort to parsing the description string (p. 245) and to give your custom exception types constructors that receive that information (p. 254).
  9. Is honest about Java design mistakes. Bloch believes in the Java platform, but he is not shy about pointing out its warts.  For example, p.86: Stack should not extend Vector and Properties should not extend HashTable.  I appreciate that honesty.
  10. Helps you design for robustness. Don’t leave room for error where you can prevent it — Bloch gives many techniques for making correct use happen naturally.
  11. Is scientifically up-to-date: Doesn’t call Pluto a planet (p.149) : )

The Bad

  1. Often doesn’t take testability into account. This is my number one gripe with the book.  Examples:
    • Item 13, “Minimize the accessibility of classes and members”, doesn’t address how its advice interacts with making a class unit testable.  In practice, there tends to be some tension between minimized accessibility and full testability.
    • Item 17, “Design and document for inheritance or else prohibit it”, similarly recommends marking classes final when they’re not designed for inheritance.  If you follow this advice naively, though, you could be setting up your code to be untestable (see the section in Michael Feathers’ Working Effectively with Legacy Code chapter 10, titled “The Case of the ‘Helpful’ Language Feature”).  Bloch does mention in passing an alternative that “provides the flexibility to use subclasses internally” (p.91), so if you have your testability goggles on that could point you in a helpful direction.
    • Item 60, “Favor the use of standard exceptions” – but it can be quite helpful to extend the standard exceptions so that test code can be certain that when an exception occurs, it was thrown by the place the test is testing.
  2. Sometimes bogs down in the details. Examples are Item 11 on cloning and Item 30 on enums, both of these being several pages longer than their sibling Items.  There were maybe five such extra long Items.
  3. Lacks numbered subheadings under Items. The book makes do with bold text, but it would be more navigable if there were numbered subsections.  This is especially the case for the longer Items.
  4. Could benefit from flowchart diagrams. Several times near the end of a chapter there is a series of questions leading to a decision tree.  For example, on p. 180:

    If you answered no to the first question, ask yourself one more: Do I want to limit the use of this marker to elements of a particular interface, forever? If so, it makes sense to define the marker as a subinterface of that interface.  If you answered no to both questions, you should probably use a marker annotation.

    It takes effort to process this prose, and I would find it helpful if such conditional logic were presented in the form of some kind of flowchart-like diagram.

  5. Directing attitude. This is the flip side of “helps you design for robustness” — not uncommonly, in the quest to close the gaps where a class could be used improperly with poor results, Bloch’s advice is to prevent, guarantee, or force.  (An example of this type of advice is Item 58, where the recommendation is to use checked exceptions to force clients to deal with an exception.   I’m not sure this is A Good Thing.)


I’ve used the word “details” a lot in my descriptions.  Bloch gives you the details you need to use Java effectively.

By reading this book I became aware that there’s more to understand about Java than I realized.  There were several things I didn’t know I didn’t know — the caveats of serialization and cloning, for instance — and some things I knew I didn’t know (concurrency comes to mind)

My biggest concern is the lack of attention given to testability.  The reader who cares about testability is on their own to work through which advice can be taken at face value and which must be modified by testability concerns.

Still, I highly recommend this book.  Its imperfections are surmountable, and every professional Java developer should understand the issues it addresses.  Even in the cases where you  may not follow Bloch’s rules,  you will gain valuable understanding of the trade-offs you’re making; and you’ll avoid many costly mistakes.

Book review: The Java Programming Language

This review covers The Java Programming Language, 4th ed. by Ken Arnold, James Gosling, and David Holmes.  Stoughton, MA: Addison Wesley, 2006.



From the preface: “This book teaches the Java programming language to people who are familiar with basic programming concepts.”

The Good

Coverage: The book makes a great attempt at covering the basic topics you need an understanding of to make good use of the language.

Authority: The book’s description of how the language works is solid.  (If, like me, you’ve ever experienced one of those programming books where the author’s statements about how the language works tend to  be on the level of “I ran a simple test on my PC and hastily draw the general conclusion that it works like X, but I don’t know any of the edge cases or caveats”, you’ll appreciate this!)

Tone: The book’s tone is not condescending.  Instruction and tips are presented in a respectful, matter-of-fact tone that doesn’t

Guidance: The book not only tells what’s possible with the language, but offers advice at times on what things might or might not be a good idea (for example, the subsections of 23.3 on Shutdown; and the footnotes on page 42, 206, 428).

Honesty: The authors aren’t afraid to occasionally say “Java does X because of a design error that at this point cannot be fixed”, or to note something that isn’t necessarily the cleanest but is there for historical reasons (examples: the spelling of Cloneable (section 3.9.1) and the footnotes on pages 85, 184, 326, 391, 408).  I find that refreshing.

Shows where you need further study: For instance, in the second paragraph of section 14.10 talking about the Java memory model, the book’s explanation of when one thread’s change to a variable is or is not guaranteed to be visible to another thread with or without synchronization made me realize that I need to find a good book just on Java concurrency to master this area.

Helped me see the big picture: A lot of work has gone into the design of the Java language, and while there are a lot of details in the book, it didn’t neglect the big picture.  The book helped me see some of the things the designers were trying to accomplish with the language.

Makes you think about design: Beginning to see the big picture (see previous point),  I also saw areas where the design of the Java language is not mistake-free, or where its designers made decisions I think I disagree with.  For instance, was it a good idea for the containers to implement a fat interface and throw UnsupportedOperationException on certain operations?  It seems to me it would have been cleaner to honor the Liskov Substitution Principle (PDF) and slim down those fat interfaces to what each collection type really must support.

Describes the old kitchen sink: The book spends a fair amount of time covering such topics as the legacy collections.  These legacy-but-not-deprecated aspects of the language are important to understand so that you know to prefer their newer replacements and can effectively read and work with code using these older constructs.

Index: A big ol’ honkin’ index.  Nice!

The Bad

Thick: Not a quick read.

Not a One-Stop Shop: Even at 760 pages not counting the index, the book doesn’t go into enough detail to confer expert knowledge of all the topics it covers.  For example, after reading the concurrency chapter, I mainly realized that this is a topic that I’ll need to study more.

Describes the old kitchen sink: The book spends a fair amount of time covering such topics as the legacy collections.  I listed this as a positive, but it’s also a negative — it’s distracting if you want to concentrate on learning how to do things the new way.

Doesn’t take testability into account: This is my biggest beef with the book.  Language constructs such as final and protected are discussed with a view toward what you’re enabling extending classes to do or preventing them from doing — and that’s good!  But the book says nothing about how  such design may affect the testability of the code, and it’s easy to accidentally write untestable code using some of those language features (see Michael Feathers’ Working Effectively with Legacy Code, Chapter 10).  So, you need to bring your existing knowledge of writing for testability with you and use discernment when the book recommends a design style that might result in difficult-to-test code.


This is a very strong book, which I heartily recommend.  Some might chafe at the somewhat slow pace, but I appreciate the book’s thorough coverage of language features and inclusion throughout of consequences, trade-offs, and caveats to know about.  For topics such as concurrency that the book did not cover in sufficient detail for me to use well, it gave enough detail to help me realize that I need further study on that topic.  It was written by folks who understand what they’re talking about, and it’s now my primary Java reference.

Versioning sets: IllegalArgumentException

We’re experiencing an issue when we try to use envers to version a persistent object that holds a set of other persistent objects, when we’re using Spring’s JTA transaction manager. We’re consistently getting a

org.springframework.transaction.UnexpectedRollbackException: JTA transaction unexpectedly rolled back (maybe due to a timeout); nested exception is transaction was marked as rollback only and has been rolled back
at org.springframework.transaction.jta.JtaTransactionManager.doCommit(
at com.ontsys.fw.testutil.transaction.TransactionManager.commit(
at com.ontsys.db.inventory.products.dao.ProductSetDAOIntegrationTest.createProduct(
at com.ontsys.db.inventory.products.dao.ProductSetDAOIntegrationTest.testRetrieveSetWithSetOfOne(

The problem doesn’t happen when we’re using Spring’s HibernateTransactionManager.

The thing that fails succeeds at first

Something that was throwing me for a loop (so to speak) was that the place that is throwing the exception succeeds several times first.  I had a breakpoint in at the call to synchronization.beforeCompletion(), but I stepped all the way into this and back out, and it worked — the first time.

Counting the times till it fails

I had the idea to keep resuming from that breakpoint to count how many times it succeeded before the exception blew up my test.  I found that it was after it hit the breakpoint in the eighth time that the beforeCompletion event never returned.  So I set the Hit Count on that breakpoint to 8 (so it wouldn’t break there until the eighth time).

Let’s see that in super slow-motion…

Turns out that it’s the seventh time it hits org.hibernate.event.def.AbstractSaveEventListener.performSave() that the exception occurs, down in a call of  The call stack at the method.invoke() call that will result in the IllegalArgumentException looks like this:$BasicSetter.set(java.lang.Object, java.lang.Object, org.hibernate.engine.SessionFactoryImplementor) line: 66	
org.hibernate.tuple.entity.PojoEntityTuplizer(org.hibernate.tuple.entity.AbstractEntityTuplizer).setIdentifier(java.lang.Object, line: 234	
org.hibernate.persister.entity.SingleTableEntityPersister(org.hibernate.persister.entity.AbstractEntityPersister).setIdentifier(java.lang.Object,, org.hibernate.EntityMode) line: 3624	
org.hibernate.event.def.DefaultSaveEventListener(org.hibernate.event.def.AbstractSaveEventListener).performSave(java.lang.Object,, org.hibernate.persister.entity.EntityPersister, boolean, java.lang.Object, org.hibernate.event.EventSource, boolean) line: 194	
org.hibernate.event.def.DefaultSaveEventListener(org.hibernate.event.def.AbstractSaveEventListener).saveWithGeneratedId(java.lang.Object, java.lang.String, java.lang.Object, org.hibernate.event.EventSource, boolean) line: 144	
org.hibernate.event.def.DefaultSaveEventListener(org.hibernate.event.def.DefaultSaveOrUpdateEventListener).saveWithGeneratedOrRequestedId(org.hibernate.event.SaveOrUpdateEvent) line: 210	
org.hibernate.event.def.DefaultSaveEventListener.saveWithGeneratedOrRequestedId(org.hibernate.event.SaveOrUpdateEvent) line: 56	
org.hibernate.event.def.DefaultSaveEventListener(org.hibernate.event.def.DefaultSaveOrUpdateEventListener).entityIsTransient(org.hibernate.event.SaveOrUpdateEvent) line: 195	
org.hibernate.event.def.DefaultSaveEventListener.performSaveOrUpdate(org.hibernate.event.SaveOrUpdateEvent) line: 50	
org.hibernate.event.def.DefaultSaveEventListener(org.hibernate.event.def.DefaultSaveOrUpdateEventListener).onSaveOrUpdate(org.hibernate.event.SaveOrUpdateEvent) line: 93	
org.hibernate.impl.SessionImpl.fireSave(org.hibernate.event.SaveOrUpdateEvent) line: 562, java.lang.Object) line: 550, java.lang.Object) line: 67	
org.jboss.envers.synchronization.VersionsSync.executeInSession(org.hibernate.Session) line: 120	
org.jboss.envers.synchronization.VersionsSync.beforeCompletion() line: 135 line: 366 line: 142 line: 96	
org.springframework.transaction.jta.JtaTransactionManager.doCommit( line: 1028	
org.springframework.transaction.jta.JtaTransactionManager( line: 732	
org.springframework.transaction.jta.JtaTransactionManager( line: 701	
com.ontsys.fw.testutil.transaction.TransactionManager.commit() line: 129	
com.ontsys.db.inventory.products.dao.ProductSetDAOIntegrationTest.createProduct(com.ontsys.db.inventory.products.po.ProductPO, java.lang.String) line: 409	
com.ontsys.db.inventory.products.dao.ProductSetDAOIntegrationTest.testRetrieveSetWithSetOfOne() line: 123	

A Closer Look at the InvalidArgumentException

What is this IllegalArgumentException that BasicPropertyAccessor.BasicSetter.set() catches when it tries the method.invoke()?  The contained message is:

object is not an instance of declaring class

BasicPropertyAccessor.BasicSetter.set() then logs these messages:

IllegalArgumentException in class: com.ontsys.fw.datatypes.BaseTO, setter method of property: key
expected type: java.lang.String, actual value: java.lang.String

(Hmm, expected a java.lang.String but got a java.lang.String, huh?) …and then throwing an exception:

org.hibernate.PropertyAccessException: IllegalArgumentException occurred while calling setter of com.ontsys.fw.datatypes.BaseTO.key

Going back to the IllegalArgumentException’s message, though: why would our object not be an instance of the declaring class?

That, my friends, will have to wait until next time.

(Can’t) automatically show the return value in the Eclipse debugger

When I’m using the Eclipse debugger, I’d sometimes like to see the return value of a method, or the contents of a thrown exception.  I can do this by selecting the expression that is about to be returned and pressing Ctrl+Shift+D (“Display”), which evaluates the expression… but this is difficult with exceptions and sometimes there are side-effects that make you not want to cause the code to be evaluated an extra time.

I wish there were a way to configure the Eclipse debugger to automatically show the return value (perhaps in the Expressions view, or maybe in the Variables view).  This appears to be a missing feature at the moment.

Here’s someone asking the same question on… and someone else asking on the old Sun Debugging Tools and Techniques forum in 2006.

Eclipse bug #40912 has been logged for this issue, but it seems to be languishing with a resolution of “LATER” since 2007 (I just added my vote, though!)

I remember this functionality being helpful when I was using Visual Studio (VS 2002, maybe?)  a few years ago.

Checked exceptions: good or bad?

Java supports checked exceptions and C# doesn’t.  A friend and I had a conversation earlier this morning about whether it’s a minus for a programming language to lack check exceptions.

My point of view

I’m not sure it’s the wrong decision, to not support checked exceptions…  I think the key is for the code to document which (unchecked) exceptions it specifically throws.

What we want:

  1. To have the ability to handle exceptions we know what to do with
  2. Not to be forced by code we call to handle exceptions we may wish to let percolate on up the call stack.

If the third-party library throws checked exceptions it FORCES the calling library to do something (if only explictly ignore), which violates the second requirement.  A given block of code should only do something with an exception if it knows what to do with it, otherwise it should percolate up to someone who does know what to do with it.  The checked exception mechanism interferes with this by forcing each caller to deal with the exception explicitly — not insurmountable, but less clean.

If library code doesn’t document the (unchecked) exceptions it throws, that would violate the first requirement, prevent good caller handling and be bad.

I think both requirements are important — that’s why I think checked exceptions are the wrong thing.

Who owns this decision?

By saying we don’t want to be forced by code we call to handle exceptions we may wish to let percolate on up the call stack I’m not saying we want to leave exceptions unhandled that we should be handling — I’m not saying I want the app to unexpectedly crash in production.  I guess what I’m really getting at is that I think the decision of which exceptions must be dealt with should be made by my application’s business logic, not the author of the library I’m using.  Why is it Sun that gets to make the decision that this particular IOException must be dealt with by the caller?  (Again, even if only by passing the buck to its caller — my point being you have to add explicit code to do something.)

An Analogy

This reminds me of an accounts receivable system that has persons, accounts, and relationships between the two.

Certain information is attached to each person — a first name, for example.

Certain other information is attached to each account — its balance, for instance.

But there is some information that doesn’t pertain directly to a person nor an account — for instance, whether the person is legally responsible for the balance on an account.  Legal responsibility isn’t a property of a person, surely (if so, then once your “legally responsible” flag was set, you’d be seen as legally responsible for all accounts you’re associated with — even if on certain accounts you were only listed as a contact).  And of course accounts aren’t legally responsible.  But a person is either responsible or not for a given account with which they’re associated.

It seems to me that with Java’s checked exceptions, the “exception must be handled ” property has been placed in the wrong hands — in the library developer’s instead of the application writer’s.  It feels like we’re placing the “legally responsible” property on the person.

A Softening?

The Spring framework wraps many Java checked exceptions with unchecked exceptions.  I searched their site for a written philosophy of checked versus unchecked exceptions, thinking I might find a categorical “checked exceptions are evil” statement with which to buttress my argument.  Instead I found this, in the Spring mission statement:

We believe that…checked exceptions are overused in Java. A platform shouldn’t force you to catch exceptions you’re unlikely to be able to recover from.

I think highly of the design decisions I’ve seen coming from the Spring group, so this more nuanced answer carries weight with me.

Maybe there’s still a proper place for checked exceptions, then…

But still, is the library author the right one to decide which “exceptions you’re unlikely to be able to recover from”?



Bad version number in .class file

Recently I’d started getting errors like this in Eclipse:

Got an exception - java.lang.UnsupportedClassVersionError: Bad version number in .class file

It’s .java files that get the error, on “Line 0”.

My (False) Suspicions

We recently switched from Java 5 to Java 6, and my JAVA_HOME environment variable was pointing to the Java 6 installation as it should.  I began to suspect that Something was causing the libraries my project depends on to be compiled under Java 5, causing the Bad Version Number issue.

A Hint

Then I read Joachim Sauer’s reply to a question on this topic, which included this quote:

Also check “java -version” [to see] if you’re indeed executing the Java version that you think you are.

I ran java -version, and to my surprise I saw:

java version "1.5.0_15"

The True Cause

Though I had updated JAVA_HOME to point to Java 6, I had not updated the system PATH, which still pointed to Java 5.  So the issue was the opposite of what I thought: The libraries my project depended on were being (properly) compiled for Java 6, but I was accidentally compiling my project for Java 5!

Fixing it

I mainly needed to update the PATH variable, but there are measures you have to take to get the changes to propagate everywhere.

Step by step, then, I…

  1. Updated the PATH to point to jdk1.6.0_11’s bin directory instead of  1.5.0_15’s
  2. Closed my command prompt and opened a new one (to get the environment changes), ran java -version, and this time saw
    java version "1.6.0_11"

    (as it should be.)

  3. Closed and reopened Eclipse, went to Project -> Clean… and selected Clean all projects

When the workspace refreshed, all the Bad version number in .class file errors disappeared!

Update 1/15/2009: Another thing to check is that in Eclipse Window -> Preferences -> Java -> Installed JREs, the Java 6 JRE’s entry is there and that its checkbox is checked.

TDD course: what would make us happy?

Back in May, when I was just getting started on my current team, we had an instructor in to do most of a week training on Java, with the last couple of days devoted to TDD training.  As the Java training progressed, I wrote down some thoughts in the process of trying to define what success would look like for the TDD portion — what would make us happy?

Here are some of the things I was thinking, that week in May…

Big idea

I’d like to spend more time on best practices and figuring out how to really tie these  practices to day-to-day reality, and less on a description of the basic process.


  1. What differences are there when using TDD for modifying existing code versus writing new  code?
  2. Where does writing acceptance tests/integration tests fit in? Is this outside the red,  green, refactor cycle?  Invite the instructor to take a look at our architecture and give him an overview of  the kind of testing we’ve done so far… hear his thoughts on holes or weaknesses, inefficiencies,  in our testing strategy?
  3. How to maintain the discipline to do TDD in the face of deadlines/schedule pressure – Is pair programming a good way of dealing with this?  How do you answer for the apparent  inefficiency of “two people working on one thing”?
  4. How does the “refactor” phase interact with the concept of code ownership?  Should anybody be  able to commit any code, because this is needed to support refactoring?  Or are some restrictions  wise?  What effects has the instructor seen in shops that have code ownership controls versus  shops that don’t?  (My feeling is that the concept of code ownership can tend to work mightily  against a clean, well-factored system and that we should rely on the tests to show if a  refactoring has broken something… I’d like to know if there are other aspects of the issue that  I’m not seeing though)
  5. How do you mesh up-front architectural analysis with TDD well? James Coplien is vocal about the importance of this:

    I’ve seen a lot of Agile projects crash and burn after about their third external iteration for  lack of good architectural foundations. It doesn’t take much architecture to shape the whole —  just enough to keep Conway’s Law happy and to lay a foundation for the interface — but it takes  much more than Ron Jeffrie’s fabled “ten minutes of design.” Maybe a sprint’s worth is enough to  get started.

  6. Martin Fowler sees value in some architectural analysis too:

    “So my advice is to begin by assessing what the likely architecture is. If you see a large amount  of data with multiple users, go ahead and use a database from day 1. If you see complex business  logic, put in a domain model. However in deference to the gods of YAGNI, when in doubt err on the  side of simplicity. Also be ready to simplify your architecture as soon as you see that part of  the architecture isn’t adding anything.”

Could be helpful

  • An instructor-demoed example showing a large refactoring performed TDD-style (in Agile Java the big refactorings were where I had more trouble seeing how to use TDD and I started making a bunch of changes while I had a red bar, not knowing how long it would take to get back to a stable state or if I was going down a dead end)
  • Another such exercise that’s hands-on, where we practice doing a large refactoring (perhaps in  groups of twos)

Other Undefined Expectations

  • How much hands-on TDD experience do we wish for?  Or, is actually doing it during class not what we want?  (for some in the class, they’ve never done it and it could be a really powerful experience to actually do it… but then others of us have….)

NullPointerException on my test’s first use of a mock

This had happened before, but it got me again the other day: I was using Unitils‘ mock annotations (great stuff!) to set up my mocks, but my test failed with a NullPointerException the first place I tried to set an expection on the mock.  The reason being… I forgot to @RunWith(UnitilsJUnit4TestClassRunner.class) .  Sadly, I don’t think I was much faster diagnosing the problem this time than I was last time.

Goal: Next time this symptom appears, instantly suspect that I’m using the wrong runner!  :)

My checkstyle regex mismatch

Checkstyle has a LineLength module.  This enables us to automatically give info messages if the code has extra long lines in it.  The LineLength module has an ignorePattern property for those long lines that are ok.  We were setting this to ignore javadoc comments with long links in them.

What it was before:


The Problem

The problem was, this regex did not match cases where the @link was on one line and the link had been formatted to the next line (as would happen sometimes when we use Eclipse’s Ctrl+Shift+F code formatter), so those still showed up as infos.

What I Want to Match

Zero or more whitespace characters,
followed by a literal ‘*’,
followed by zero or more whitespace characters,
optionally followed by ‘@see ‘ or ‘{@link ‘,
followed by one or more non-whitespace characters.

My Good-enough Solution

After messing around for quite a while, this is the regex I eventually put in place:

^.*\*.*((@see)|(\{@link) )?[^ \t]+$

Notice the .* after the initial ^ though: we’re matching on any character, not just whitespace.  I wanted to do something like this:

^\s*\*\s*((@see)|(\{@link) )?[^ \t]+$

Or this:

^[ \t]*\*[ \t]*((@see)|(\{@link) )?[^ \t]+$

But when I did that, the comment lines weren’t matched, and they still showed up as infos.

Why is that?